mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' into 'production'
Development See merge request mbugroup/lti-web-client!427
This commit is contained in:
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
npm run format
|
npm run format
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run typecheck
|
npm run typecheck
|
||||||
|
git add .
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import Dropdown from '@/components/dropdown/Dropdown';
|
|||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
|
||||||
@@ -51,7 +51,16 @@ type ExpenseTableFilters = {
|
|||||||
transactionDate: string;
|
transactionDate: string;
|
||||||
realizationDate: string;
|
realizationDate: string;
|
||||||
locationId: string;
|
locationId: string;
|
||||||
|
locationName: string;
|
||||||
vendorId: string;
|
vendorId: string;
|
||||||
|
vendorName: string;
|
||||||
|
category: string;
|
||||||
|
approvalStatus: string;
|
||||||
|
realizationStatus: string;
|
||||||
|
projectFlockId: string;
|
||||||
|
projectFlockName: string;
|
||||||
|
projectFlockKandangId: string;
|
||||||
|
projectFlockKandangName: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,43 +84,6 @@ type ApprovalStatusValue =
|
|||||||
const isApprovalDateRequired = (status?: ApprovalStatusValue) =>
|
const isApprovalDateRequired = (status?: ApprovalStatusValue) =>
|
||||||
status === 'REALISASI' || status === 'SELESAI';
|
status === 'REALISASI' || status === 'SELESAI';
|
||||||
|
|
||||||
const getExportErrorMessage = async (
|
|
||||||
error: unknown,
|
|
||||||
fallbackMessage: string
|
|
||||||
) => {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
const responseData = error.response?.data;
|
|
||||||
|
|
||||||
if (responseData instanceof Blob) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(await responseData.text()) as {
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
return parsed.message || fallbackMessage;
|
|
||||||
} catch {
|
|
||||||
return fallbackMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
responseData &&
|
|
||||||
typeof responseData === 'object' &&
|
|
||||||
'message' in responseData &&
|
|
||||||
typeof responseData.message === 'string'
|
|
||||||
) {
|
|
||||||
return responseData.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.message || fallbackMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
popoverPosition = 'bottom',
|
popoverPosition = 'bottom',
|
||||||
props,
|
props,
|
||||||
@@ -235,6 +207,7 @@ const ExpensesTable = () => {
|
|||||||
setPage,
|
setPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
toQueryString: getTableFilterQueryString,
|
toQueryString: getTableFilterQueryString,
|
||||||
|
reset: resetFilter,
|
||||||
} = useTableFilter<ExpenseTableFilters>({
|
} = useTableFilter<ExpenseTableFilters>({
|
||||||
initial: {
|
initial: {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -244,7 +217,16 @@ const ExpensesTable = () => {
|
|||||||
transactionDate: '',
|
transactionDate: '',
|
||||||
realizationDate: '',
|
realizationDate: '',
|
||||||
locationId: '',
|
locationId: '',
|
||||||
|
locationName: '',
|
||||||
vendorId: '',
|
vendorId: '',
|
||||||
|
vendorName: '',
|
||||||
|
category: '',
|
||||||
|
approvalStatus: '',
|
||||||
|
realizationStatus: '',
|
||||||
|
projectFlockId: '',
|
||||||
|
projectFlockName: '',
|
||||||
|
projectFlockKandangId: '',
|
||||||
|
projectFlockKandangName: '',
|
||||||
userId: '',
|
userId: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
@@ -254,7 +236,16 @@ const ExpensesTable = () => {
|
|||||||
transactionDate: 'transaction_date',
|
transactionDate: 'transaction_date',
|
||||||
realizationDate: 'realization_date',
|
realizationDate: 'realization_date',
|
||||||
locationId: 'location_id',
|
locationId: 'location_id',
|
||||||
|
locationName: 'location_name',
|
||||||
vendorId: 'vendor_id',
|
vendorId: 'vendor_id',
|
||||||
|
vendorName: 'vendor_name',
|
||||||
|
category: 'category',
|
||||||
|
approvalStatus: 'approval_status',
|
||||||
|
realizationStatus: 'realization_status',
|
||||||
|
projectFlockId: 'project_flock_id',
|
||||||
|
projectFlockName: 'project_flock_name',
|
||||||
|
projectFlockKandangId: 'project_flock_kandang_id',
|
||||||
|
projectFlockKandangName: 'project_flock_kandang_name',
|
||||||
userId: 'user_id',
|
userId: 'user_id',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -286,6 +277,8 @@ const ExpensesTable = () => {
|
|||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||||
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
|
useState(false);
|
||||||
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
const [bulkApprovalStatus, setBulkApprovalStatus] =
|
const [bulkApprovalStatus, setBulkApprovalStatus] =
|
||||||
@@ -575,7 +568,7 @@ const ExpensesTable = () => {
|
|||||||
toast.success('Ekspor berhasil');
|
toast.success('Ekspor berhasil');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
await getExportErrorMessage(error, 'Gagal mengekspor input progress')
|
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsExportProgressLoading(false);
|
setIsExportProgressLoading(false);
|
||||||
@@ -740,22 +733,70 @@ const ExpensesTable = () => {
|
|||||||
const handleFilterSubmit = (values: {
|
const handleFilterSubmit = (values: {
|
||||||
transaction_date?: string | null;
|
transaction_date?: string | null;
|
||||||
realization_date?: string | null;
|
realization_date?: string | null;
|
||||||
location_id?: string | null;
|
location?: { value: number; label: string } | null;
|
||||||
vendor_id?: string | null;
|
vendor?: { value: number; label: string } | null;
|
||||||
|
category?: OptionType<string> | null;
|
||||||
|
approval_status?: OptionType<string> | null;
|
||||||
|
realization_status?: OptionType<string> | null;
|
||||||
|
project_flock?: OptionType<number> | null;
|
||||||
|
project_flock_kandang?: OptionType<number> | null;
|
||||||
}) => {
|
}) => {
|
||||||
updateFilter('transactionDate', values.transaction_date || '');
|
updateFilter('transactionDate', values.transaction_date || '');
|
||||||
updateFilter('realizationDate', values.realization_date || '');
|
updateFilter('realizationDate', values.realization_date || '');
|
||||||
updateFilter('locationId', values.location_id || '');
|
updateFilter(
|
||||||
updateFilter('vendorId', values.vendor_id || '');
|
'locationId',
|
||||||
|
values.location?.value ? String(values.location?.value) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'locationName',
|
||||||
|
values.location?.label ? String(values.location?.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'vendorId',
|
||||||
|
values.vendor?.value ? String(values.vendor?.value) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'vendorName',
|
||||||
|
values.vendor?.label ? String(values.vendor?.label) : ''
|
||||||
|
);
|
||||||
|
updateFilter('category', values.category?.value || '');
|
||||||
|
updateFilter('approvalStatus', values.approval_status?.value || '');
|
||||||
|
updateFilter('realizationStatus', values.realization_status?.value || '');
|
||||||
|
updateFilter(
|
||||||
|
'projectFlockId',
|
||||||
|
values.project_flock?.value ? String(values.project_flock.value) : ''
|
||||||
|
);
|
||||||
|
updateFilter('projectFlockName', values.project_flock?.label || '');
|
||||||
|
updateFilter(
|
||||||
|
'projectFlockKandangId',
|
||||||
|
values.project_flock_kandang?.value
|
||||||
|
? String(values.project_flock_kandang.value)
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'projectFlockKandangName',
|
||||||
|
values.project_flock_kandang?.label || ''
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterReset = () => {
|
const handleFilterReset = () => {
|
||||||
updateFilter('transactionDate', '');
|
resetFilter();
|
||||||
updateFilter('realizationDate', '');
|
|
||||||
updateFilter('locationId', '');
|
|
||||||
updateFilter('vendorId', '');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportToExcel = useCallback(async () => {
|
||||||
|
setIsLoadingExportingToExcel(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ExpenseApi.exportToExcel(getTableFilterQueryString());
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
await getErrorMessage(error, 'Gagal mengekspor data pengeluaran')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingExportingToExcel(false);
|
||||||
|
}
|
||||||
|
}, [getTableFilterQueryString]);
|
||||||
|
|
||||||
// track sorting
|
// track sorting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
@@ -927,6 +968,10 @@ const ExpensesTable = () => {
|
|||||||
'search',
|
'search',
|
||||||
'nameSort',
|
'nameSort',
|
||||||
'userId',
|
'userId',
|
||||||
|
'locationName',
|
||||||
|
'vendorName',
|
||||||
|
'projectFlockName',
|
||||||
|
'projectFlockKandangName',
|
||||||
]}
|
]}
|
||||||
onClick={handleFilterModalOpen}
|
onClick={handleFilterModalOpen}
|
||||||
className='px-3 py-2.5'
|
className='px-3 py-2.5'
|
||||||
@@ -965,6 +1010,17 @@ const ExpensesTable = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportToExcel}
|
||||||
|
isLoading={isLoadingExportingToExcel}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Ekspor ke Excel
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
@@ -1245,6 +1301,59 @@ const ExpensesTable = () => {
|
|||||||
ref={filterModal.ref}
|
ref={filterModal.ref}
|
||||||
onSubmit={handleFilterSubmit}
|
onSubmit={handleFilterSubmit}
|
||||||
onReset={handleFilterReset}
|
onReset={handleFilterReset}
|
||||||
|
initialValues={{
|
||||||
|
location:
|
||||||
|
tableFilterState.locationId && tableFilterState.locationName
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.locationId),
|
||||||
|
label: tableFilterState.locationName,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
vendor:
|
||||||
|
tableFilterState.vendorId && tableFilterState.vendorName
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.vendorId),
|
||||||
|
label: tableFilterState.vendorName,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
realization_date: tableFilterState.realizationDate,
|
||||||
|
transaction_date: tableFilterState.transactionDate,
|
||||||
|
category: tableFilterState.category
|
||||||
|
? {
|
||||||
|
value: tableFilterState.category,
|
||||||
|
label: tableFilterState.category,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
approval_status: tableFilterState.approvalStatus
|
||||||
|
? approvalStatusOptions.find(
|
||||||
|
(item) => item.value === tableFilterState.approvalStatus
|
||||||
|
) || null
|
||||||
|
: null,
|
||||||
|
realization_status: tableFilterState.realizationStatus
|
||||||
|
? [
|
||||||
|
{ value: 'NOT_REALIZED', label: 'Belum Realisasi' },
|
||||||
|
{ value: 'REALIZED', label: 'Sudah Realisasi' },
|
||||||
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
|
].find(
|
||||||
|
(item) => item.value === tableFilterState.realizationStatus
|
||||||
|
) || null
|
||||||
|
: null,
|
||||||
|
project_flock:
|
||||||
|
tableFilterState.projectFlockId && tableFilterState.projectFlockName
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.projectFlockId),
|
||||||
|
label: tableFilterState.projectFlockName,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
project_flock_kandang:
|
||||||
|
tableFilterState.projectFlockKandangId &&
|
||||||
|
tableFilterState.projectFlockKandangName
|
||||||
|
? {
|
||||||
|
value: Number(tableFilterState.projectFlockKandangId),
|
||||||
|
label: tableFilterState.projectFlockKandangName,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,13 @@ import * as yup from 'yup';
|
|||||||
export type ExpensesFilterType = {
|
export type ExpensesFilterType = {
|
||||||
transaction_date: string | null;
|
transaction_date: string | null;
|
||||||
realization_date: string | null;
|
realization_date: string | null;
|
||||||
location_id: string | null;
|
location: { value: number; label: string } | null;
|
||||||
vendor_id: string | null;
|
vendor: { value: number; label: string } | null;
|
||||||
|
category: { value: string; label: string } | null;
|
||||||
|
approval_status: { value: string; label: string } | null;
|
||||||
|
realization_status: { value: string; label: string } | null;
|
||||||
|
project_flock: { value: number; label: string } | null;
|
||||||
|
project_flock_kandang: { value: number; label: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExpensesFilterSchema = yup.object({
|
export const ExpensesFilterSchema = yup.object({
|
||||||
@@ -21,8 +26,48 @@ export const ExpensesFilterSchema = yup.object({
|
|||||||
return new Date(value) >= new Date(transaction_date);
|
return new Date(value) >= new Date(transaction_date);
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
location_id: yup.string().nullable(),
|
location: yup
|
||||||
vendor_id: yup.string().nullable(),
|
.object({
|
||||||
|
value: yup.number().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
vendor: yup
|
||||||
|
.object({
|
||||||
|
value: yup.number().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
category: yup
|
||||||
|
.object({
|
||||||
|
value: yup.string().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
approval_status: yup
|
||||||
|
.object({
|
||||||
|
value: yup.string().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
realization_status: yup
|
||||||
|
.object({
|
||||||
|
value: yup.string().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
project_flock: yup
|
||||||
|
.object({
|
||||||
|
value: yup.number().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
project_flock_kandang: yup
|
||||||
|
.object({
|
||||||
|
value: yup.number().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
export type ExpensesFilterValues = yup.InferType<typeof ExpensesFilterSchema>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject } from 'react';
|
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -11,8 +11,11 @@ import SelectInput from '@/components/input/SelectInput';
|
|||||||
|
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
import { LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import {
|
import {
|
||||||
ExpensesFilterSchema,
|
ExpensesFilterSchema,
|
||||||
ExpensesFilterValues,
|
ExpensesFilterValues,
|
||||||
@@ -31,64 +34,143 @@ const ExpensesFilterModal = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onReset,
|
onReset,
|
||||||
}: ExpensesFilterModalProps) => {
|
}: ExpensesFilterModalProps) => {
|
||||||
|
const [selectedLocationId, setSelectedLocationId] = useState<string>(
|
||||||
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
|
);
|
||||||
const closeModalHandler = () => {
|
const closeModalHandler = () => {
|
||||||
ref.current?.close();
|
ref.current?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const categoryOptions = [
|
||||||
|
{ value: 'BOP', label: 'BOP' },
|
||||||
|
{ value: 'NON-BOP', label: 'NON-BOP' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const approvalStatusOptions = [
|
||||||
|
{ value: 'HEAD_AREA', label: 'Approval Head Area' },
|
||||||
|
{ value: 'UNIT_VICE_PRESIDENT', label: 'Approval Unit Vice President' },
|
||||||
|
{ value: 'FINANCE', label: 'Approval Finance' },
|
||||||
|
{ value: 'REALISASI', label: 'Realisasi' },
|
||||||
|
{ value: 'SELESAI', label: 'Selesai' },
|
||||||
|
{ value: 'DITOLAK', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const realizationStatusOptions = [
|
||||||
|
{ value: 'NOT_REALIZED', label: 'Belum Realisasi' },
|
||||||
|
{ value: 'REALIZED', label: 'Sudah Realisasi' },
|
||||||
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setVendorInputValue,
|
setInputValue: setVendorInputValue,
|
||||||
options: vendorOptions,
|
options: vendorOptions,
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
|
loadMore: loadMoreVendors,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProjectFlockInputValue,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
options: projectFlockOptions,
|
||||||
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
location_id: selectedLocationId || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const formik = useFormik<ExpensesFilterValues>({
|
const formik = useFormik<ExpensesFilterValues>({
|
||||||
|
enableReinitialize: true,
|
||||||
initialValues: initialValues || {
|
initialValues: initialValues || {
|
||||||
transaction_date: null,
|
transaction_date: null,
|
||||||
realization_date: null,
|
realization_date: null,
|
||||||
location_id: null,
|
location: null,
|
||||||
vendor_id: null,
|
vendor: null,
|
||||||
|
category: null,
|
||||||
|
approval_status: null,
|
||||||
|
realization_status: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
},
|
},
|
||||||
validationSchema: ExpensesFilterSchema,
|
validationSchema: ExpensesFilterSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
onSubmit?.(values);
|
onSubmit?.(values);
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
|
||||||
onReset?.();
|
|
||||||
closeModalHandler();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const locationValue = formik.values.location_id
|
useEffect(() => {
|
||||||
? locationOptions.find(
|
setSelectedLocationId(
|
||||||
(opt) => String(opt.value) === formik.values.location_id
|
initialValues?.location?.value ? String(initialValues.location.value) : ''
|
||||||
) || null
|
);
|
||||||
: null;
|
}, [initialValues?.location]);
|
||||||
|
|
||||||
const vendorValue = formik.values.vendor_id
|
const { resetForm } = formik;
|
||||||
? vendorOptions.find(
|
|
||||||
(opt) => String(opt.value) === formik.values.vendor_id
|
const formikResetHandler = useCallback(() => {
|
||||||
) || null
|
resetForm({
|
||||||
: null;
|
values: {
|
||||||
|
transaction_date: null,
|
||||||
|
realization_date: null,
|
||||||
|
location: null,
|
||||||
|
vendor: null,
|
||||||
|
category: null,
|
||||||
|
approval_status: null,
|
||||||
|
realization_status: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSelectedLocationId('');
|
||||||
|
onReset?.();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset, closeModalHandler]);
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const locationId =
|
const value = val as OptionType | null;
|
||||||
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
formik.setFieldValue('location', value);
|
||||||
formik.setFieldValue('location_id', locationId);
|
formik.setFieldValue('project_flock', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
setSelectedLocationId(value?.value ? String(value.value) : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
const vendorId =
|
formik.setFieldValue('vendor', val as OptionType | null);
|
||||||
val && !Array.isArray(val) ? (String(val.value) as string) : null;
|
|
||||||
formik.setFieldValue('vendor_id', vendorId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!formik.values.project_flock ||
|
||||||
|
!projectFlocksRawData ||
|
||||||
|
!isResponseSuccess(projectFlocksRawData)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProjectFlock = projectFlocksRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.project_flock?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedProjectFlock?.kandangs?.map((item) => ({
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock, projectFlocksRawData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -98,7 +180,7 @@ const ExpensesFilterModal = ({
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={formik.handleSubmit}
|
onSubmit={formik.handleSubmit}
|
||||||
onReset={formik.handleReset}
|
onReset={formikResetHandler}
|
||||||
className='w-full flex flex-col'
|
className='w-full flex flex-col'
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
@@ -160,10 +242,11 @@ const ExpensesFilterModal = ({
|
|||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
placeholder='Pilih Lokasi'
|
placeholder='Pilih Lokasi'
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
value={locationValue}
|
value={formik.values.location}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isClearable
|
isClearable
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
@@ -173,14 +256,87 @@ const ExpensesFilterModal = ({
|
|||||||
label='Vendor'
|
label='Vendor'
|
||||||
placeholder='Pilih Vendor'
|
placeholder='Pilih Vendor'
|
||||||
options={vendorOptions}
|
options={vendorOptions}
|
||||||
value={vendorValue}
|
value={formik.values.vendor}
|
||||||
onChange={vendorChangeHandler}
|
onChange={vendorChangeHandler}
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
isLoading={isLoadingVendorOptions}
|
isLoading={isLoadingVendorOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreVendors}
|
||||||
isClearable
|
isClearable
|
||||||
isSearchable={true}
|
isSearchable={true}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={categoryOptions}
|
||||||
|
value={formik.values.category}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('category', val as OptionType | null)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status BOP'
|
||||||
|
placeholder='Pilih Status BOP'
|
||||||
|
options={approvalStatusOptions}
|
||||||
|
value={formik.values.approval_status}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('approval_status', val as OptionType | null)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status Pencairan'
|
||||||
|
placeholder='Pilih Status Pencairan'
|
||||||
|
options={realizationStatusOptions}
|
||||||
|
value={formik.values.realization_status}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'realization_status',
|
||||||
|
val as OptionType | null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
options={projectFlockOptions}
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue('project_flock', val as OptionType | null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
}}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
isClearable
|
||||||
|
isSearchable={true}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
options={projectFlockKandangOptions}
|
||||||
|
value={formik.values.project_flock_kandang}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_kandang',
|
||||||
|
val as OptionType | null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.project_flock}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { MarketingApi } from '@/services/api/marketing/marketing';
|
|||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
|
||||||
interface MarketingFilterModal {
|
interface MarketingFilterModal {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
@@ -78,6 +80,19 @@ const MarketingFilterModal = ({
|
|||||||
has_marketing: 'true',
|
has_marketing: 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
options: projectFlockOptions,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
setInputValue: setProjectFlockInputValue,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
...MARKETING_APPROVAL_LINE.map((item) => ({
|
...MARKETING_APPROVAL_LINE.map((item) => ({
|
||||||
value: item.step_name.split(' ').join('_').toUpperCase(),
|
value: item.step_name.split(' ').join('_').toUpperCase(),
|
||||||
@@ -91,6 +106,8 @@ const MarketingFilterModal = ({
|
|||||||
product_ids: [],
|
product_ids: [],
|
||||||
status: null,
|
status: null,
|
||||||
customer: null,
|
customer: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
},
|
},
|
||||||
validationSchema: MarketingFilterSchema,
|
validationSchema: MarketingFilterSchema,
|
||||||
|
|
||||||
@@ -99,6 +116,9 @@ const MarketingFilterModal = ({
|
|||||||
product_ids: values.product_ids.map((item) => Number(item.value)),
|
product_ids: values.product_ids.map((item) => Number(item.value)),
|
||||||
status: values.status?.value.toString() || '',
|
status: values.status?.value.toString() || '',
|
||||||
customer_id: Number(values.customer?.value),
|
customer_id: Number(values.customer?.value),
|
||||||
|
project_flock_id: Number(values.project_flock?.value) || undefined,
|
||||||
|
project_flock_kandang_id:
|
||||||
|
Number(values.project_flock_kandang?.value) || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit?.(formattedValues);
|
onSubmit?.(formattedValues);
|
||||||
@@ -126,6 +146,27 @@ const MarketingFilterModal = ({
|
|||||||
formik.setFieldValue('status', val as OptionType);
|
formik.setFieldValue('status', val as OptionType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!formik.values.project_flock ||
|
||||||
|
!projectFlocksRawData ||
|
||||||
|
!isResponseSuccess(projectFlocksRawData)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProjectFlock = projectFlocksRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.project_flock?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedProjectFlock?.kandangs?.map((item) => ({
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock, projectFlocksRawData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -192,6 +233,37 @@ const MarketingFilterModal = ({
|
|||||||
onInputChange={setCustomersInputValue}
|
onInputChange={setCustomersInputValue}
|
||||||
onMenuScrollToBottom={loadMoreCustomers}
|
onMenuScrollToBottom={loadMoreCustomers}
|
||||||
/>
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
options={projectFlockOptions}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock',
|
||||||
|
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
||||||
|
);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
}}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
options={projectFlockKandangOptions}
|
||||||
|
value={formik.values.project_flock_kandang}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_kandang',
|
||||||
|
!Array.isArray(val) ? (val as OptionType<number> | null) : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isDisabled={!formik.values.project_flock}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import Modal, { useModal } from '@/components/Modal';
|
|||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import {
|
||||||
|
getErrorMessage,
|
||||||
|
isResponseError,
|
||||||
|
isResponseSuccess,
|
||||||
|
} from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
MarketingApi,
|
MarketingApi,
|
||||||
@@ -37,43 +41,6 @@ import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter';
|
|||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import MarketingTableSkeleton from '@/components/pages/marketing/skeleton/MarketingTableSkeleton';
|
import MarketingTableSkeleton from '@/components/pages/marketing/skeleton/MarketingTableSkeleton';
|
||||||
|
|
||||||
const getExportErrorMessage = async (
|
|
||||||
error: unknown,
|
|
||||||
fallbackMessage: string
|
|
||||||
) => {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
const responseData = error.response?.data;
|
|
||||||
|
|
||||||
if (responseData instanceof Blob) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(await responseData.text()) as {
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
return parsed.message || fallbackMessage;
|
|
||||||
} catch {
|
|
||||||
return fallbackMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
responseData &&
|
|
||||||
typeof responseData === 'object' &&
|
|
||||||
'message' in responseData &&
|
|
||||||
typeof responseData.message === 'string'
|
|
||||||
) {
|
|
||||||
return responseData.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.message || fallbackMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RowsOptionsMenu = ({
|
const RowsOptionsMenu = ({
|
||||||
props,
|
props,
|
||||||
deleteClickHandler,
|
deleteClickHandler,
|
||||||
@@ -224,6 +191,8 @@ const MarketingTable = () => {
|
|||||||
product_ids: '',
|
product_ids: '',
|
||||||
status: '',
|
status: '',
|
||||||
customer_id: '',
|
customer_id: '',
|
||||||
|
project_flock_id: '',
|
||||||
|
project_flock_kandang_id: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -231,6 +200,8 @@ const MarketingTable = () => {
|
|||||||
product_ids: 'product_ids',
|
product_ids: 'product_ids',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
customer_id: 'customer_id',
|
customer_id: 'customer_id',
|
||||||
|
project_flock_id: 'project_flock_id',
|
||||||
|
project_flock_kandang_id: 'project_flock_kandang_id',
|
||||||
},
|
},
|
||||||
|
|
||||||
persist: true,
|
persist: true,
|
||||||
@@ -260,6 +231,18 @@ const MarketingTable = () => {
|
|||||||
values.customer_id ? values.customer_id.toString() : '',
|
values.customer_id ? values.customer_id.toString() : '',
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_id',
|
||||||
|
values.project_flock_id ? values.project_flock_id.toString() : '',
|
||||||
|
true
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_kandang_id',
|
||||||
|
values.project_flock_kandang_id
|
||||||
|
? values.project_flock_kandang_id.toString()
|
||||||
|
: '',
|
||||||
|
true
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
@@ -269,6 +252,8 @@ const MarketingTable = () => {
|
|||||||
updateFilter('product_ids', '', true);
|
updateFilter('product_ids', '', true);
|
||||||
updateFilter('status', '', true);
|
updateFilter('status', '', true);
|
||||||
updateFilter('customer_id', '', true);
|
updateFilter('customer_id', '', true);
|
||||||
|
updateFilter('project_flock_id', '', true);
|
||||||
|
updateFilter('project_flock_kandang_id', '', true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveClickHandler = () => {
|
const approveClickHandler = () => {
|
||||||
@@ -519,7 +504,7 @@ const MarketingTable = () => {
|
|||||||
toast.success('Ekspor berhasil');
|
toast.success('Ekspor berhasil');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
await getExportErrorMessage(error, 'Gagal mengekspor input progress')
|
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsExportProgressLoading(false);
|
setIsExportProgressLoading(false);
|
||||||
@@ -835,7 +820,7 @@ const MarketingTable = () => {
|
|||||||
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
Export to Excel
|
Ekspor ke Excel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ export const MarketingFilterSchema = object({
|
|||||||
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
|
product_ids: array().of(mixed<OptionType<number>>().required()).required(),
|
||||||
status: mixed<OptionType<string>>().nullable(),
|
status: mixed<OptionType<string>>().nullable(),
|
||||||
customer: mixed<OptionType<number>>().nullable(),
|
customer: mixed<OptionType<number>>().nullable(),
|
||||||
|
project_flock: mixed<OptionType<number>>().nullable(),
|
||||||
|
project_flock_kandang: mixed<OptionType<number>>().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MarketingFilterFormValues = {
|
export type MarketingFilterFormValues = {
|
||||||
product_ids: OptionType<number>[];
|
product_ids: OptionType<number>[];
|
||||||
status: OptionType<string> | null;
|
status: OptionType<string> | null;
|
||||||
customer: OptionType<number> | null;
|
customer: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import Table from '@/components/Table';
|
|||||||
import { type Recording } from '@/types/api/production/recording';
|
import { type Recording } from '@/types/api/production/recording';
|
||||||
import { getRecordingRestriction } from './recording-utils';
|
import { getRecordingRestriction } from './recording-utils';
|
||||||
import { RecordingApi } from '@/services/api/production';
|
import { RecordingApi } from '@/services/api/production';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import StatusBadge from '@/components/helper/StatusBadge';
|
import StatusBadge from '@/components/helper/StatusBadge';
|
||||||
@@ -52,43 +52,6 @@ import { Color } from '@/types/theme';
|
|||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
|
||||||
const getExportErrorMessage = async (
|
|
||||||
error: unknown,
|
|
||||||
fallbackMessage: string
|
|
||||||
) => {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
const responseData = error.response?.data;
|
|
||||||
|
|
||||||
if (responseData instanceof Blob) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(await responseData.text()) as {
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
return parsed.message || fallbackMessage;
|
|
||||||
} catch {
|
|
||||||
return fallbackMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
responseData &&
|
|
||||||
typeof responseData === 'object' &&
|
|
||||||
'message' in responseData &&
|
|
||||||
typeof responseData.message === 'string'
|
|
||||||
) {
|
|
||||||
return responseData.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.message || fallbackMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== STATUS BADGE UTILITIES =====
|
// ===== STATUS BADGE UTILITIES =====
|
||||||
const statusTextMap: Record<string, string> = {
|
const statusTextMap: Record<string, string> = {
|
||||||
APPROVED: 'Disetujui',
|
APPROVED: 'Disetujui',
|
||||||
@@ -319,15 +282,23 @@ const RecordingTable = () => {
|
|||||||
search: '',
|
search: '',
|
||||||
areaFilter: '',
|
areaFilter: '',
|
||||||
locationFilter: '',
|
locationFilter: '',
|
||||||
|
projectFlockFilter: '',
|
||||||
kandangFilter: '',
|
kandangFilter: '',
|
||||||
projectFlockKandangFilter: '',
|
projectFlockKandangFilter: '',
|
||||||
|
approvalStatusFilter: '',
|
||||||
|
projectFlockCategoryFilter: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
search: 'search',
|
search: 'search',
|
||||||
|
areaFilter: 'area_id',
|
||||||
|
locationFilter: 'location_id',
|
||||||
|
projectFlockFilter: 'project_flock_id',
|
||||||
kandangFilter: 'kandang_id',
|
kandangFilter: 'kandang_id',
|
||||||
projectFlockKandangFilter: 'project_flock_kandang_id',
|
projectFlockKandangFilter: 'project_flock_kandang_id',
|
||||||
|
approvalStatusFilter: 'approval_status',
|
||||||
|
projectFlockCategoryFilter: 'project_flock_category',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -356,26 +327,38 @@ const RecordingTable = () => {
|
|||||||
initialValues: {
|
initialValues: {
|
||||||
area_id: null,
|
area_id: null,
|
||||||
location_id: null,
|
location_id: null,
|
||||||
|
project_flock_id: null,
|
||||||
kandang_id: null,
|
kandang_id: null,
|
||||||
project_flock_kandang_id: null,
|
project_flock_kandang_id: null,
|
||||||
|
approval_status: null,
|
||||||
|
project_flock_category: null,
|
||||||
},
|
},
|
||||||
validationSchema: RecordingFilterSchema,
|
validationSchema: RecordingFilterSchema,
|
||||||
onSubmit: (values, { setSubmitting }) => {
|
onSubmit: (values, { setSubmitting }) => {
|
||||||
updateFilter('areaFilter', values.area_id || '');
|
updateFilter('areaFilter', values.area_id || '');
|
||||||
updateFilter('locationFilter', values.location_id || '');
|
updateFilter('locationFilter', values.location_id || '');
|
||||||
|
updateFilter('projectFlockFilter', values.project_flock_id || '');
|
||||||
updateFilter('kandangFilter', values.kandang_id || '');
|
updateFilter('kandangFilter', values.kandang_id || '');
|
||||||
updateFilter(
|
updateFilter(
|
||||||
'projectFlockKandangFilter',
|
'projectFlockKandangFilter',
|
||||||
values.project_flock_kandang_id || ''
|
values.project_flock_kandang_id || ''
|
||||||
);
|
);
|
||||||
|
updateFilter('approvalStatusFilter', values.approval_status || '');
|
||||||
|
updateFilter(
|
||||||
|
'projectFlockCategoryFilter',
|
||||||
|
values.project_flock_category || ''
|
||||||
|
);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
updateFilter('areaFilter', '');
|
updateFilter('areaFilter', '');
|
||||||
updateFilter('locationFilter', '');
|
updateFilter('locationFilter', '');
|
||||||
|
updateFilter('projectFlockFilter', '');
|
||||||
updateFilter('kandangFilter', '');
|
updateFilter('kandangFilter', '');
|
||||||
updateFilter('projectFlockKandangFilter', '');
|
updateFilter('projectFlockKandangFilter', '');
|
||||||
|
updateFilter('approvalStatusFilter', '');
|
||||||
|
updateFilter('projectFlockCategoryFilter', '');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -537,6 +520,7 @@ const RecordingTable = () => {
|
|||||||
|
|
||||||
formik.setFieldValue('area_id', areaId);
|
formik.setFieldValue('area_id', areaId);
|
||||||
formik.setFieldValue('location_id', null);
|
formik.setFieldValue('location_id', null);
|
||||||
|
formik.setFieldValue('project_flock_id', null);
|
||||||
formik.setFieldValue('kandang_id', null);
|
formik.setFieldValue('kandang_id', null);
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
|
|
||||||
@@ -556,6 +540,7 @@ const RecordingTable = () => {
|
|||||||
const locationId = location?.value ? String(location.value) : null;
|
const locationId = location?.value ? String(location.value) : null;
|
||||||
|
|
||||||
formik.setFieldValue('location_id', locationId);
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
formik.setFieldValue('project_flock_id', null);
|
||||||
formik.setFieldValue('kandang_id', null);
|
formik.setFieldValue('kandang_id', null);
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
|
|
||||||
@@ -570,7 +555,11 @@ const RecordingTable = () => {
|
|||||||
const handleFilterProjectFlockChange = useCallback(
|
const handleFilterProjectFlockChange = useCallback(
|
||||||
(val: OptionType | OptionType[] | null) => {
|
(val: OptionType | OptionType[] | null) => {
|
||||||
const projectFlock = val as OptionType | null;
|
const projectFlock = val as OptionType | null;
|
||||||
|
const projectFlockId = projectFlock?.value
|
||||||
|
? String(projectFlock.value)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
formik.setFieldValue('project_flock_id', projectFlockId);
|
||||||
formik.setFieldValue('kandang_id', null);
|
formik.setFieldValue('kandang_id', null);
|
||||||
formik.setFieldValue('project_flock_kandang_id', null);
|
formik.setFieldValue('project_flock_kandang_id', null);
|
||||||
|
|
||||||
@@ -625,6 +614,36 @@ const RecordingTable = () => {
|
|||||||
);
|
);
|
||||||
}, [formik.values.kandang_id, kandangOptions]);
|
}, [formik.values.kandang_id, kandangOptions]);
|
||||||
|
|
||||||
|
const recordingApprovalStatusOptions: OptionType<string>[] = [
|
||||||
|
{ value: 'CREATED', label: 'Pengajuan' },
|
||||||
|
{ value: 'UPDATED', label: 'Diperbarui' },
|
||||||
|
{ value: 'APPROVED', label: 'Disetujui' },
|
||||||
|
{ value: 'REJECTED', label: 'Ditolak' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const projectFlockCategoryOptions: OptionType<string>[] = [
|
||||||
|
{ value: 'GROWING', label: 'Growing' },
|
||||||
|
{ value: 'LAYING', label: 'Laying' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const approvalStatusValue = useMemo(() => {
|
||||||
|
if (!formik.values.approval_status) return null;
|
||||||
|
return (
|
||||||
|
recordingApprovalStatusOptions.find(
|
||||||
|
(opt) => opt.value === formik.values.approval_status
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.approval_status]);
|
||||||
|
|
||||||
|
const projectFlockCategoryValue = useMemo(() => {
|
||||||
|
if (!formik.values.project_flock_category) return null;
|
||||||
|
return (
|
||||||
|
projectFlockCategoryOptions.find(
|
||||||
|
(opt) => opt.value === formik.values.project_flock_category
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock_category]);
|
||||||
|
|
||||||
// ===== HANDLE FILTER MODAL OPEN =====
|
// ===== HANDLE FILTER MODAL OPEN =====
|
||||||
const handleFilterModalOpen = () => {
|
const handleFilterModalOpen = () => {
|
||||||
filterModal.openModal();
|
filterModal.openModal();
|
||||||
@@ -783,7 +802,7 @@ const RecordingTable = () => {
|
|||||||
toast.success('Ekspor berhasil');
|
toast.success('Ekspor berhasil');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
await getExportErrorMessage(error, 'Gagal mengekspor input progress')
|
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsExportProgressLoading(false);
|
setIsExportProgressLoading(false);
|
||||||
@@ -1607,6 +1626,36 @@ const RecordingTable = () => {
|
|||||||
isDisabled={!filterProjectFlock}
|
isDisabled={!filterProjectFlock}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori'
|
||||||
|
placeholder='Pilih Kategori'
|
||||||
|
options={projectFlockCategoryOptions}
|
||||||
|
value={projectFlockCategoryValue}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_category',
|
||||||
|
!Array.isArray(val) && val ? String(val.value) : null
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Status Approval'
|
||||||
|
placeholder='Pilih Status Approval'
|
||||||
|
options={recordingApprovalStatusOptions}
|
||||||
|
value={approvalStatusValue}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'approval_status',
|
||||||
|
!Array.isArray(val) && val ? String(val.value) : null
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isClearable
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Footer */}
|
{/* Modal Footer */}
|
||||||
@@ -1631,11 +1680,7 @@ const RecordingTable = () => {
|
|||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||||
disabled={
|
disabled={!formik.isValid || formik.isSubmitting}
|
||||||
!formik.isValid ||
|
|
||||||
formik.isSubmitting ||
|
|
||||||
!formik.values.kandang_id
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -3,13 +3,19 @@ import { string, object } from 'yup';
|
|||||||
export const RecordingFilterSchema = object().shape({
|
export const RecordingFilterSchema = object().shape({
|
||||||
area_id: string().nullable(),
|
area_id: string().nullable(),
|
||||||
location_id: string().nullable(),
|
location_id: string().nullable(),
|
||||||
|
project_flock_id: string().nullable(),
|
||||||
kandang_id: string().nullable(),
|
kandang_id: string().nullable(),
|
||||||
project_flock_kandang_id: string().nullable(),
|
project_flock_kandang_id: string().nullable(),
|
||||||
|
approval_status: string().nullable(),
|
||||||
|
project_flock_category: string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RecordingFilterType = {
|
export type RecordingFilterType = {
|
||||||
area_id: string | null;
|
area_id: string | null;
|
||||||
location_id: string | null;
|
location_id: string | null;
|
||||||
|
project_flock_id: string | null;
|
||||||
kandang_id: string | null;
|
kandang_id: string | null;
|
||||||
project_flock_kandang_id: string | null;
|
project_flock_kandang_id: string | null;
|
||||||
|
approval_status: string | null;
|
||||||
|
project_flock_category: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { RefObject, useState, useEffect } from 'react';
|
import { RefObject, useState, useEffect, useMemo } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -9,12 +9,20 @@ import Modal from '@/components/Modal';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
|
import SelectInput from '@/components/input/SelectInput';
|
||||||
|
|
||||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||||
import { PurchaseFilter } from '@/types/api/purchase/purchase';
|
import { PurchaseFilter } from '@/types/api/purchase/purchase';
|
||||||
|
import { AreaApi, LocationApi, SupplierApi } from '@/services/api/master-data';
|
||||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
|
import { Area } from '@/types/api/master-data/area';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
|
||||||
interface PurchaseFilterModalProps {
|
interface PurchaseFilterModalProps {
|
||||||
ref: RefObject<HTMLDialogElement | null>;
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
@@ -73,32 +81,112 @@ const PurchaseFilterModal = ({
|
|||||||
'search'
|
'search'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [selectedAreaId, setSelectedAreaId] = useState('');
|
||||||
|
const [selectedLocationId, setSelectedLocationId] = useState('');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setSupplierInputValue,
|
||||||
|
options: supplierOptions,
|
||||||
|
isLoadingOptions: isLoadingSupplierOptions,
|
||||||
|
loadMore: loadMoreSuppliers,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setAreaInputValue,
|
||||||
|
options: areaOptions,
|
||||||
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
|
} = useSelect<Area>(AreaApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
|
area_id: selectedAreaId || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProjectFlockInputValue,
|
||||||
|
options: projectFlockOptions,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
} = useSelect<ProjectFlock>(
|
||||||
|
ProjectFlockApi.basePath,
|
||||||
|
'id',
|
||||||
|
'flock_name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
location_id: selectedLocationId || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const formik = useFormik<{
|
const formik = useFormik<{
|
||||||
poDate: string;
|
poDate: string;
|
||||||
category: { label: string; value: number }[];
|
category: { label: string; value: number }[];
|
||||||
status: { label: string; value: string }[];
|
status: { label: string; value: string }[];
|
||||||
|
supplier: OptionType<number> | null;
|
||||||
|
area: OptionType<number> | null;
|
||||||
|
location: OptionType<number> | null;
|
||||||
|
project_flock: OptionType<number> | null;
|
||||||
|
project_flock_kandang: OptionType<number> | null;
|
||||||
}>({
|
}>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
poDate: '',
|
poDate: '',
|
||||||
category: [],
|
category: [],
|
||||||
status: [],
|
status: [],
|
||||||
|
supplier: null,
|
||||||
|
area: null,
|
||||||
|
location: null,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_kandang: null,
|
||||||
},
|
},
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
const formattedValues = {
|
const formattedValues = {
|
||||||
...values,
|
...values,
|
||||||
category: values.category.map((item) => String(item.value)),
|
category: values.category.map((item) => String(item.value)),
|
||||||
status: values.status.map((item) => String(item.value)),
|
status: values.status.map((item) => String(item.value)),
|
||||||
|
supplier_id: values.supplier?.value,
|
||||||
|
area_id: values.area?.value,
|
||||||
|
location_id: values.location?.value,
|
||||||
|
project_flock_id: values.project_flock?.value,
|
||||||
|
project_flock_kandang_id: values.project_flock_kandang?.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit?.(formattedValues);
|
onSubmit?.(formattedValues);
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
onReset: () => {
|
onReset: () => {
|
||||||
|
setSelectedAreaId('');
|
||||||
|
setSelectedLocationId('');
|
||||||
onReset?.();
|
onReset?.();
|
||||||
closeModalHandler();
|
closeModalHandler();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const projectFlockKandangOptions = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!formik.values.project_flock ||
|
||||||
|
!projectFlocksRawData ||
|
||||||
|
!isResponseSuccess(projectFlocksRawData)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProjectFlock = projectFlocksRawData.data.find(
|
||||||
|
(item) => item.id === formik.values.project_flock?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
selectedProjectFlock?.kandangs?.map((item) => ({
|
||||||
|
value: item.project_flock_kandang_id,
|
||||||
|
label: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [formik.values.project_flock, projectFlocksRawData]);
|
||||||
|
|
||||||
const productCategoryChangeHandler = (
|
const productCategoryChangeHandler = (
|
||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
) => {
|
) => {
|
||||||
@@ -172,6 +260,108 @@ const PurchaseFilterModal = ({
|
|||||||
value: item.step_name,
|
value: item.step_name,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Vendor'
|
||||||
|
placeholder='Pilih Vendor'
|
||||||
|
value={formik.values.supplier}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'supplier',
|
||||||
|
!Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options={supplierOptions}
|
||||||
|
isLoading={isLoadingSupplierOptions}
|
||||||
|
onInputChange={setSupplierInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
value={formik.values.area}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = !Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null;
|
||||||
|
formik.setFieldValue('area', nextValue);
|
||||||
|
formik.setFieldValue('location', null);
|
||||||
|
formik.setFieldValue('project_flock', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
setSelectedAreaId(
|
||||||
|
nextValue?.value ? String(nextValue.value) : ''
|
||||||
|
);
|
||||||
|
setSelectedLocationId('');
|
||||||
|
}}
|
||||||
|
options={areaOptions}
|
||||||
|
isLoading={isLoadingAreaOptions}
|
||||||
|
onInputChange={setAreaInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
value={formik.values.location}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = !Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null;
|
||||||
|
formik.setFieldValue('location', nextValue);
|
||||||
|
formik.setFieldValue('project_flock', null);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
setSelectedLocationId(
|
||||||
|
nextValue?.value ? String(nextValue.value) : ''
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
options={locationOptions}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.area}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Project Flock'
|
||||||
|
placeholder='Pilih Project Flock'
|
||||||
|
value={formik.values.project_flock}
|
||||||
|
onChange={(val) => {
|
||||||
|
const nextValue = !Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null;
|
||||||
|
formik.setFieldValue('project_flock', nextValue);
|
||||||
|
formik.setFieldValue('project_flock_kandang', null);
|
||||||
|
}}
|
||||||
|
options={projectFlockOptions}
|
||||||
|
isLoading={isLoadingProjectFlockOptions}
|
||||||
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.location}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
value={formik.values.project_flock_kandang}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
'project_flock_kandang',
|
||||||
|
!Array.isArray(val)
|
||||||
|
? (val as OptionType<number> | null)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options={projectFlockKandangOptions}
|
||||||
|
isClearable
|
||||||
|
isDisabled={!formik.values.project_flock}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal
|
|||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
import Dropdown from '@/components/dropdown/Dropdown';
|
||||||
|
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -43,43 +43,6 @@ import { ExpenseApi } from '@/services/api/expense';
|
|||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
|
||||||
const getExportErrorMessage = async (
|
|
||||||
error: unknown,
|
|
||||||
fallbackMessage: string
|
|
||||||
) => {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
const responseData = error.response?.data;
|
|
||||||
|
|
||||||
if (responseData instanceof Blob) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(await responseData.text()) as {
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
return parsed.message || fallbackMessage;
|
|
||||||
} catch {
|
|
||||||
return fallbackMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
responseData &&
|
|
||||||
typeof responseData === 'object' &&
|
|
||||||
'message' in responseData &&
|
|
||||||
typeof responseData.message === 'string'
|
|
||||||
) {
|
|
||||||
return responseData.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return error.message || fallbackMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== STATUS BADGE UTILITIES =====
|
// ===== STATUS BADGE UTILITIES =====
|
||||||
const statusTextMap: Record<string, string> = {
|
const statusTextMap: Record<string, string> = {
|
||||||
APPROVED: 'Disetujui',
|
APPROVED: 'Disetujui',
|
||||||
@@ -192,6 +155,8 @@ const PurchaseTable = () => {
|
|||||||
|
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
|
useState(false);
|
||||||
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
const [isExportProgressLoading, setIsExportProgressLoading] = useState(false);
|
||||||
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
|
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
|
||||||
null
|
null
|
||||||
@@ -213,6 +178,11 @@ const PurchaseTable = () => {
|
|||||||
po_date: '',
|
po_date: '',
|
||||||
approval_status: '',
|
approval_status: '',
|
||||||
product_category_id: '',
|
product_category_id: '',
|
||||||
|
supplier_id: '',
|
||||||
|
area_id: '',
|
||||||
|
location_id: '',
|
||||||
|
project_flock_id: '',
|
||||||
|
project_flock_kandang_id: '',
|
||||||
},
|
},
|
||||||
paramMap: {
|
paramMap: {
|
||||||
page: 'page',
|
page: 'page',
|
||||||
@@ -220,6 +190,11 @@ const PurchaseTable = () => {
|
|||||||
po_date: 'po_date',
|
po_date: 'po_date',
|
||||||
approval_status: 'approval_status',
|
approval_status: 'approval_status',
|
||||||
product_category_id: 'product_category_id',
|
product_category_id: 'product_category_id',
|
||||||
|
supplier_id: 'supplier_id',
|
||||||
|
area_id: 'area_id',
|
||||||
|
location_id: 'location_id',
|
||||||
|
project_flock_id: 'project_flock_id',
|
||||||
|
project_flock_kandang_id: 'project_flock_kandang_id',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -467,14 +442,52 @@ const PurchaseTable = () => {
|
|||||||
updateFilter('po_date', values.poDate);
|
updateFilter('po_date', values.poDate);
|
||||||
updateFilter('product_category_id', values.category.join(','));
|
updateFilter('product_category_id', values.category.join(','));
|
||||||
updateFilter('approval_status', values.status.join(','));
|
updateFilter('approval_status', values.status.join(','));
|
||||||
|
updateFilter(
|
||||||
|
'supplier_id',
|
||||||
|
values.supplier_id ? String(values.supplier_id) : ''
|
||||||
|
);
|
||||||
|
updateFilter('area_id', values.area_id ? String(values.area_id) : '');
|
||||||
|
updateFilter(
|
||||||
|
'location_id',
|
||||||
|
values.location_id ? String(values.location_id) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_id',
|
||||||
|
values.project_flock_id ? String(values.project_flock_id) : ''
|
||||||
|
);
|
||||||
|
updateFilter(
|
||||||
|
'project_flock_kandang_id',
|
||||||
|
values.project_flock_kandang_id
|
||||||
|
? String(values.project_flock_kandang_id)
|
||||||
|
: ''
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterResetHandler = () => {
|
const filterResetHandler = () => {
|
||||||
updateFilter('po_date', '');
|
updateFilter('po_date', '');
|
||||||
updateFilter('product_category_id', '');
|
updateFilter('product_category_id', '');
|
||||||
updateFilter('approval_status', '');
|
updateFilter('approval_status', '');
|
||||||
|
updateFilter('supplier_id', '');
|
||||||
|
updateFilter('area_id', '');
|
||||||
|
updateFilter('location_id', '');
|
||||||
|
updateFilter('project_flock_id', '');
|
||||||
|
updateFilter('project_flock_kandang_id', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportToExcel = useCallback(async () => {
|
||||||
|
setIsLoadingExportingToExcel(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await PurchaseApi.exportToExcel(getTableFilterQueryString());
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
await getErrorMessage(error, 'Gagal mengekspor data pembelian')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingExportingToExcel(false);
|
||||||
|
}
|
||||||
|
}, [getTableFilterQueryString]);
|
||||||
|
|
||||||
const resetExportProgressForm = useCallback(() => {
|
const resetExportProgressForm = useCallback(() => {
|
||||||
setExportProgressStartDate('');
|
setExportProgressStartDate('');
|
||||||
setExportProgressEndDate('');
|
setExportProgressEndDate('');
|
||||||
@@ -513,7 +526,7 @@ const PurchaseTable = () => {
|
|||||||
toast.success('Ekspor berhasil');
|
toast.success('Ekspor berhasil');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
await getExportErrorMessage(error, 'Gagal mengekspor input progress')
|
await getErrorMessage(error, 'Gagal mengekspor input progress')
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsExportProgressLoading(false);
|
setIsExportProgressLoading(false);
|
||||||
@@ -610,6 +623,17 @@ const PurchaseTable = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={exportToExcel}
|
||||||
|
isLoading={isLoadingExportingToExcel}
|
||||||
|
className='w-full p-3 justify-start text-sm text-base-content/50 font-semibold text-nowrap'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:table-cells' width={20} height={20} />
|
||||||
|
Ekspor ke Excel
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='none'
|
color='none'
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ const PurchaseRequestForm = ({
|
|||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const [, setLocationSelectInputValue] = useState('');
|
|
||||||
const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>(
|
const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -163,6 +162,7 @@ const PurchaseRequestForm = ({
|
|||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
loadMore: loadMoreLocations,
|
loadMore: loadMoreLocations,
|
||||||
|
setInputValue: setLocationSelectInputValue,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
area_id:
|
area_id:
|
||||||
selectedArea != ''
|
selectedArea != ''
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import Table from '@/components/Table';
|
|||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { ReportExpense } from '@/types/api/report/report-expense';
|
import { ReportExpense } from '@/types/api/report/report-expense';
|
||||||
import { ReportExpenseApi } from '@/services/api/report/expense-report';
|
import { ReportExpenseApi } from '@/services/api/report/expense-report';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { getErrorMessage, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store';
|
||||||
import Modal, { useModal } from '@/components/Modal';
|
import Modal, { useModal } from '@/components/Modal';
|
||||||
import Pagination from '@/components/Pagination';
|
import Pagination from '@/components/Pagination';
|
||||||
@@ -189,26 +189,47 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
[formik.values.category]
|
[formik.values.category]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const buildReportExpenseQueryString = useCallback(
|
||||||
|
(extraParams?: Record<string, string>) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (filterParams.location_id) {
|
||||||
|
params.append('location_id', filterParams.location_id);
|
||||||
|
}
|
||||||
|
if (filterParams.supplier_id) {
|
||||||
|
params.append('supplier_id', filterParams.supplier_id);
|
||||||
|
}
|
||||||
|
if (filterParams.kandang_id) {
|
||||||
|
params.append('project_flock_kandang_id', filterParams.kandang_id);
|
||||||
|
}
|
||||||
|
if (filterParams.nonstock_id) {
|
||||||
|
params.append('nonstock_id', filterParams.nonstock_id);
|
||||||
|
}
|
||||||
|
if (filterParams.realization_date) {
|
||||||
|
params.append('realization_date', filterParams.realization_date);
|
||||||
|
}
|
||||||
|
if (filterParams.category) {
|
||||||
|
params.append('category', filterParams.category);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(extraParams ?? {}).forEach(([key, value]) => {
|
||||||
|
params.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return params.toString();
|
||||||
|
},
|
||||||
|
[filterParams]
|
||||||
|
);
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: reportExpenseResponse, isLoading } = useSWR(
|
const { data: reportExpenseResponse, isLoading } = useSWR(
|
||||||
() => {
|
() => {
|
||||||
const params = new URLSearchParams();
|
const queryString = buildReportExpenseQueryString({
|
||||||
if (filterParams.location_id)
|
page: String(page),
|
||||||
params.append('location_id', filterParams.location_id);
|
limit: String(pageSize),
|
||||||
if (filterParams.supplier_id)
|
});
|
||||||
params.append('supplier_id', filterParams.supplier_id);
|
|
||||||
if (filterParams.kandang_id)
|
|
||||||
params.append('project_flock_kandang_id', filterParams.kandang_id);
|
|
||||||
if (filterParams.nonstock_id)
|
|
||||||
params.append('nonstock_id', filterParams.nonstock_id);
|
|
||||||
if (filterParams.realization_date)
|
|
||||||
params.append('realization_date', filterParams.realization_date);
|
|
||||||
if (filterParams.category)
|
|
||||||
params.append('category', filterParams.category);
|
|
||||||
params.append('page', String(page));
|
|
||||||
params.append('limit', String(pageSize));
|
|
||||||
|
|
||||||
return [`${ReportExpenseApi.basePath}?${params.toString()}`];
|
return [`${ReportExpenseApi.basePath}?${queryString}`];
|
||||||
},
|
},
|
||||||
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
([url]: string[]) => httpClient<BaseApiResponse<ReportExpense[]>>(url)
|
||||||
);
|
);
|
||||||
@@ -233,47 +254,31 @@ const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => {
|
|||||||
const reportExpenseExport = useCallback(async (): Promise<
|
const reportExpenseExport = useCallback(async (): Promise<
|
||||||
ReportExpense[] | null
|
ReportExpense[] | null
|
||||||
> => {
|
> => {
|
||||||
const params = new URLSearchParams();
|
const queryString = buildReportExpenseQueryString({
|
||||||
if (filterParams.location_id)
|
page: '1',
|
||||||
params.append('location_id', filterParams.location_id);
|
limit: '100',
|
||||||
if (filterParams.supplier_id)
|
});
|
||||||
params.append('supplier_id', filterParams.supplier_id);
|
|
||||||
if (filterParams.kandang_id)
|
|
||||||
params.append('kandang_id', filterParams.kandang_id);
|
|
||||||
if (filterParams.nonstock_id)
|
|
||||||
params.append('nonstock_id', filterParams.nonstock_id);
|
|
||||||
if (filterParams.realization_date)
|
|
||||||
params.append('realization_date', filterParams.realization_date);
|
|
||||||
if (filterParams.category) params.append('category', filterParams.category);
|
|
||||||
params.append('limit', '100');
|
|
||||||
params.append('page', '1');
|
|
||||||
|
|
||||||
const response = await httpClient<BaseApiResponse<ReportExpense[]>>(
|
const response = await httpClient<BaseApiResponse<ReportExpense[]>>(
|
||||||
`${ReportExpenseApi.basePath}?${params.toString()}`
|
`${ReportExpenseApi.basePath}?${queryString}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response) ? response.data : null;
|
return isResponseSuccess(response) ? response.data : null;
|
||||||
}, [filterParams]);
|
}, [buildReportExpenseQueryString]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
setIsExcelExportLoading(true);
|
setIsExcelExportLoading(true);
|
||||||
try {
|
try {
|
||||||
const allDataForExport = await reportExpenseExport();
|
await ReportExpenseApi.exportToExcel(buildReportExpenseQueryString());
|
||||||
|
} catch (error) {
|
||||||
if (!allDataForExport || allDataForExport.length === 0) {
|
toast.error(
|
||||||
toast.error('Tidak ada data untuk diekspor.');
|
await getErrorMessage(error, 'Gagal mengekspor data pengeluaran')
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
await generateReportExpenseExcel(allDataForExport);
|
|
||||||
toast.success('Excel berhasil dibuat dan diunduh.');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal membuat Excel. Silakan coba lagi.');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsExcelExportLoading(false);
|
setIsExcelExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [reportExpenseExport]);
|
}, [buildReportExpenseQueryString]);
|
||||||
|
|
||||||
const handleExportPDF = useCallback(async () => {
|
const handleExportPDF = useCallback(async () => {
|
||||||
setIsPdfExportLoading(true);
|
setIsPdfExportLoading(true);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const CATEGORIES = [
|
|||||||
{ value: 'pullet_close', label: 'Pullet Close' },
|
{ value: 'pullet_close', label: 'Pullet Close' },
|
||||||
{ value: 'produksi_open', label: 'Produksi Open' },
|
{ value: 'produksi_open', label: 'Produksi Open' },
|
||||||
{ value: 'produksi_close', label: 'Produksi Close' },
|
{ value: 'produksi_close', label: 'Produksi Close' },
|
||||||
|
{ value: 'empty_kandang', label: 'Kandang Kosong' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
||||||
@@ -94,6 +95,8 @@ export function DailyChecklistContent() {
|
|||||||
const [selectedCategory, setSelectedCategory] = useState(
|
const [selectedCategory, setSelectedCategory] = useState(
|
||||||
searchParams.get('category') || ''
|
searchParams.get('category') || ''
|
||||||
);
|
);
|
||||||
|
const [emptyKandang, setEmptyKandang] = useState(false);
|
||||||
|
const [emptyKandangEndDate, setEmptyKandangEndDate] = useState('');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
@@ -225,6 +228,22 @@ export function DailyChecklistContent() {
|
|||||||
}
|
}
|
||||||
}, [date, kandangId, selectedCategory, pathname, router, searchParams]);
|
}, [date, kandangId, selectedCategory, pathname, router, searchParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!emptyKandang) {
|
||||||
|
setEmptyKandangEndDate('');
|
||||||
|
setSelectedCategory('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedCategory('empty_kandang');
|
||||||
|
}, [emptyKandang]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCategory === 'empty_kandang') {
|
||||||
|
setEmptyKandang(true);
|
||||||
|
}
|
||||||
|
}, [selectedCategory]);
|
||||||
|
|
||||||
// Format date for display
|
// Format date for display
|
||||||
const formatDateForDisplay = (dateStr: string) => {
|
const formatDateForDisplay = (dateStr: string) => {
|
||||||
if (!dateStr) return 'Pilih tanggal';
|
if (!dateStr) return 'Pilih tanggal';
|
||||||
@@ -246,7 +265,7 @@ export function DailyChecklistContent() {
|
|||||||
// Check for existing checklist when unique key changes
|
// Check for existing checklist when unique key changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAndLoadChecklist = async () => {
|
const checkAndLoadChecklist = async () => {
|
||||||
if (!date || !kandangId || !selectedCategory) {
|
if (!date || !kandangId || (!emptyKandang && !selectedCategory)) {
|
||||||
setDailyChecklistId(null);
|
setDailyChecklistId(null);
|
||||||
setChecklistStatus('DRAFT');
|
setChecklistStatus('DRAFT');
|
||||||
// setIsEditMode(false);
|
// setIsEditMode(false);
|
||||||
@@ -257,12 +276,24 @@ export function DailyChecklistContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (emptyKandang && !emptyKandangEndDate) {
|
||||||
|
setDailyChecklistId(null);
|
||||||
|
setChecklistStatus('DRAFT');
|
||||||
|
setSelectedPhaseIds([]);
|
||||||
|
setActivitiesByPhase({});
|
||||||
|
setTaskIdsByPhaseActivityId({});
|
||||||
|
setAssignments({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const checklist = await DailyChecklistApi.create({
|
const checklist = await DailyChecklistApi.create({
|
||||||
date,
|
date,
|
||||||
kandang_id: Number(kandangId),
|
kandang_id: Number(kandangId),
|
||||||
category: selectedCategory,
|
category: emptyKandang ? 'empty_kandang' : selectedCategory,
|
||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
|
empty_kandang: emptyKandang,
|
||||||
|
empty_kandang_end_date: emptyKandang ? emptyKandangEndDate : '',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isResponseError(checklist)) {
|
if (isResponseError(checklist)) {
|
||||||
@@ -313,7 +344,7 @@ export function DailyChecklistContent() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
checkAndLoadChecklist();
|
checkAndLoadChecklist();
|
||||||
}, [date, kandangId, selectedCategory]);
|
}, [date, kandangId, selectedCategory, emptyKandang, emptyKandangEndDate]);
|
||||||
|
|
||||||
// Load activities and tasks when phases change
|
// Load activities and tasks when phases change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -1034,7 +1065,7 @@ export function DailyChecklistContent() {
|
|||||||
<Select
|
<Select
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onValueChange={setSelectedCategory}
|
onValueChange={setSelectedCategory}
|
||||||
disabled={!isChecklistStatusDraft}
|
disabled={!isChecklistStatusDraft || emptyKandang}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id='category'
|
id='category'
|
||||||
@@ -1053,6 +1084,39 @@ export function DailyChecklistContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-6 pb-6 border-b border-gray-200'>
|
||||||
|
<div className='flex flex-col gap-4 md:flex-row md:items-end md:gap-6'>
|
||||||
|
<label className='flex items-center gap-2 text-sm font-medium text-gray-900'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={emptyKandang}
|
||||||
|
onChange={(e) => setEmptyKandang(e.target.checked)}
|
||||||
|
disabled={!isChecklistStatusDraft}
|
||||||
|
className='checkbox-clean'
|
||||||
|
/>
|
||||||
|
<span>Kandang Kosong</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{emptyKandang && (
|
||||||
|
<div className='w-full md:max-w-md'>
|
||||||
|
<Label htmlFor='empty_kandang_end_date'>
|
||||||
|
Tanggal Akhir Kandang Kosong{' '}
|
||||||
|
<span className='text-red-500'>*</span>
|
||||||
|
</Label>
|
||||||
|
<div className='mt-1.5'>
|
||||||
|
<DatePicker
|
||||||
|
date={emptyKandangEndDate}
|
||||||
|
onDateChange={setEmptyKandangEndDate}
|
||||||
|
disabled={!isChecklistStatusDraft}
|
||||||
|
placeholder='Pilih tanggal akhir kandang kosong'
|
||||||
|
formatDisplay={formatDateForDisplay}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Phase Selection Section */}
|
{/* Phase Selection Section */}
|
||||||
{dailyChecklistId && (
|
{dailyChecklistId && (
|
||||||
<div className='mb-6 pb-6 border-b border-gray-200'>
|
<div className='mb-6 pb-6 border-b border-gray-200'>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
pullet_close: 'Pullet Close',
|
pullet_close: 'Pullet Close',
|
||||||
produksi_open: 'Produksi Open',
|
produksi_open: 'Produksi Open',
|
||||||
produksi_close: 'Produksi Close',
|
produksi_close: 'Produksi Close',
|
||||||
|
empty_kandang: 'Kandang Kosong',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ListDailyChecklistContent() {
|
export function ListDailyChecklistContent() {
|
||||||
|
|||||||
@@ -217,7 +217,9 @@ export function MasterEmployeeContent() {
|
|||||||
'Error creating employee:',
|
'Error creating employee:',
|
||||||
createEmployeeResponse.message
|
createEmployeeResponse.message
|
||||||
);
|
);
|
||||||
toast.error('Gagal menambahkan ABK');
|
toast.error(
|
||||||
|
'Gagal menambahkan ABK: ' + createEmployeeResponse.message
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +240,9 @@ export function MasterEmployeeContent() {
|
|||||||
'Error updating employee:',
|
'Error updating employee:',
|
||||||
updateEmployeeResponse.message
|
updateEmployeeResponse.message
|
||||||
);
|
);
|
||||||
toast.error('Gagal menambahkan ABK');
|
toast.error(
|
||||||
|
'Gagal memperbarui ABK: ' + updateEmployeeResponse.message
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
BaseApiResponse,
|
BaseApiResponse,
|
||||||
ErrorApiResponse,
|
ErrorApiResponse,
|
||||||
@@ -15,3 +16,40 @@ export const isResponseError = <T>(
|
|||||||
): res is ErrorApiResponse => {
|
): res is ErrorApiResponse => {
|
||||||
return res?.status === 'error';
|
return res?.status === 'error';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getErrorMessage = async (
|
||||||
|
error: unknown,
|
||||||
|
fallbackMessage: string
|
||||||
|
) => {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const responseData = error.response?.data;
|
||||||
|
|
||||||
|
if (responseData instanceof Blob) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(await responseData.text()) as {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
return parsed.message || fallbackMessage;
|
||||||
|
} catch {
|
||||||
|
return fallbackMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
responseData &&
|
||||||
|
typeof responseData === 'object' &&
|
||||||
|
'message' in responseData &&
|
||||||
|
typeof responseData.message === 'string'
|
||||||
|
) {
|
||||||
|
return responseData.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.message || fallbackMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackMessage;
|
||||||
|
};
|
||||||
|
|||||||
@@ -708,6 +708,33 @@ export class ExpenseApiService extends BaseApiService<
|
|||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async exportToExcel(initialQueryString: string) {
|
||||||
|
const params = new URLSearchParams(initialQueryString);
|
||||||
|
|
||||||
|
params.set('export', 'excel');
|
||||||
|
params.set('type', 'all');
|
||||||
|
params.set('page', '1');
|
||||||
|
params.set('limit', '99999999999');
|
||||||
|
|
||||||
|
const queryString = `?${params.toString()}`;
|
||||||
|
|
||||||
|
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([res]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
const fileName = `BOP-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
|
|
||||||
async exportInputProgressToExcel(startDate: string, endDate: string) {
|
async exportInputProgressToExcel(startDate: string, endDate: string) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
|||||||
@@ -149,67 +149,31 @@ class MarketingExportService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Export to Excel
|
|
||||||
*/
|
|
||||||
async exportToExcel(initialQueryString: string) {
|
async exportToExcel(initialQueryString: string) {
|
||||||
const params = new URLSearchParams(initialQueryString);
|
const params = new URLSearchParams(initialQueryString);
|
||||||
|
|
||||||
|
params.set('export', 'excel');
|
||||||
|
params.set('type', 'all');
|
||||||
|
params.set('page', '1');
|
||||||
|
params.set('limit', '99999999999');
|
||||||
|
|
||||||
const queryString = `?${params.toString()}`;
|
const queryString = `?${params.toString()}`;
|
||||||
|
|
||||||
try {
|
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
|
||||||
const marketingData = await httpClientFetcher<
|
method: 'GET',
|
||||||
BaseApiResponse<Marketing[]>
|
responseType: 'blob',
|
||||||
>(`${this.basePath}${queryString}`);
|
});
|
||||||
|
|
||||||
if (isResponseError(marketingData)) {
|
const url = window.URL.createObjectURL(new Blob([res]));
|
||||||
toast.error('Gagal melakukan export marketing! Coba lagi.');
|
const link = document.createElement('a');
|
||||||
return;
|
link.href = url;
|
||||||
}
|
|
||||||
|
|
||||||
const rows = marketingData.data;
|
const fileName = `penjualan-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
|
||||||
const formattedRows = [];
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
for (let i = 0; i < rows.length; i++) {
|
link.remove();
|
||||||
const row = rows[i];
|
|
||||||
const approval = row.latest_approval;
|
|
||||||
const isRejected = approval?.action === 'REJECTED';
|
|
||||||
|
|
||||||
// Calculate grand total from sales_order products
|
|
||||||
const grandTotal =
|
|
||||||
row.sales_order
|
|
||||||
?.map((product) => product.total_price)
|
|
||||||
.reduce((a, b) => a + b, 0) ?? 0;
|
|
||||||
|
|
||||||
// Get product names
|
|
||||||
const products =
|
|
||||||
row.sales_order
|
|
||||||
?.map((product) => product.product_warehouse?.product?.name)
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ') ?? '';
|
|
||||||
|
|
||||||
formattedRows.push({
|
|
||||||
'No. Order': row.so_number,
|
|
||||||
Tanggal: formatDate(row.so_date, 'DD-MM-YYYY'),
|
|
||||||
Status: isRejected
|
|
||||||
? 'Ditolak'
|
|
||||||
: formatTitleCase(approval?.step_name || ''),
|
|
||||||
Customer: row.customer?.name || '',
|
|
||||||
'Grand Total': formatCurrency(grandTotal),
|
|
||||||
Products: products,
|
|
||||||
Notes: row.notes || '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const ws = XLSX.utils.json_to_sheet(formattedRows);
|
|
||||||
const wb = XLSX.utils.book_new();
|
|
||||||
XLSX.utils.book_append_sheet(wb, ws, 'marketing');
|
|
||||||
|
|
||||||
// triggers download in browser
|
|
||||||
XLSX.writeFile(wb, 'marketing.xlsx');
|
|
||||||
} catch {
|
|
||||||
toast.error('Gagal melakukan export marketing! Coba lagi.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportInputProgressToExcel(startDate: string, endDate: string) {
|
async exportInputProgressToExcel(startDate: string, endDate: string) {
|
||||||
|
|||||||
@@ -115,6 +115,33 @@ export const PurchaseApi = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async exportToExcel(initialQueryString: string) {
|
||||||
|
const params = new URLSearchParams(initialQueryString);
|
||||||
|
|
||||||
|
params.set('export', 'excel');
|
||||||
|
params.set('type', 'all');
|
||||||
|
params.set('page', '1');
|
||||||
|
params.set('limit', '99999999999');
|
||||||
|
|
||||||
|
const queryString = `?${params.toString()}`;
|
||||||
|
|
||||||
|
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([res]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
const fileName = `pembelian-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
},
|
||||||
|
|
||||||
async exportInputProgressToExcel(startDate: string, endDate: string) {
|
async exportInputProgressToExcel(startDate: string, endDate: string) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { formatDate } from '@/lib/helper';
|
||||||
import { BaseApiService } from '@/services/api/base';
|
import { BaseApiService } from '@/services/api/base';
|
||||||
import { httpClientFetcher } from '@/services/http/client';
|
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import {
|
import {
|
||||||
ReportDepreciation,
|
ReportDepreciation,
|
||||||
@@ -20,6 +21,33 @@ export class ReportExpenseApiService extends BaseApiService<
|
|||||||
): Promise<BaseApiResponse<ReportExpense[]>> {
|
): Promise<BaseApiResponse<ReportExpense[]>> {
|
||||||
return await httpClientFetcher<BaseApiResponse<ReportExpense[]>>(endpoint);
|
return await httpClientFetcher<BaseApiResponse<ReportExpense[]>>(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async exportToExcel(initialQueryString: string) {
|
||||||
|
const params = new URLSearchParams(initialQueryString);
|
||||||
|
|
||||||
|
params.set('export', 'excel');
|
||||||
|
params.set('type', 'all');
|
||||||
|
params.set('page', '1');
|
||||||
|
params.set('limit', '99999999999');
|
||||||
|
|
||||||
|
const queryString = `?${params.toString()}`;
|
||||||
|
|
||||||
|
const res = await httpClient<Blob>(`${this.basePath}${queryString}`, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([res]));
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
const fileName = `Laporan-BOP-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`;
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReportExpenseApi = new ReportExpenseApiService('/reports/expense');
|
export const ReportExpenseApi = new ReportExpenseApiService('/reports/expense');
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export type BaseDailyChecklist = {
|
|||||||
status: string;
|
status: string;
|
||||||
category: string;
|
category: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
empty_kandang?: boolean;
|
||||||
|
empty_kandang_end_date?: string | null;
|
||||||
kandang?: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
|
kandang?: Pick<BaseKandang, 'id' | 'name' | 'status' | 'capacity'>;
|
||||||
total_phase: number;
|
total_phase: number;
|
||||||
total_activity: number;
|
total_activity: number;
|
||||||
@@ -57,6 +59,8 @@ export type CreateDailyChecklistPayload = {
|
|||||||
kandang_id: number;
|
kandang_id: number;
|
||||||
category: string;
|
category: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
empty_kandang: boolean;
|
||||||
|
empty_kandang_end_date: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PerformanceOverviewItem = {
|
export type PerformanceOverviewItem = {
|
||||||
|
|||||||
+2
@@ -97,6 +97,8 @@ export type MarketingFilter = {
|
|||||||
product_ids: number[];
|
product_ids: number[];
|
||||||
status: string;
|
status: string;
|
||||||
customer_id: number;
|
customer_id: number;
|
||||||
|
project_flock_id?: number;
|
||||||
|
project_flock_kandang_id?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Vendored
+5
@@ -149,4 +149,9 @@ export type PurchaseFilter = {
|
|||||||
poDate: string;
|
poDate: string;
|
||||||
category: string[];
|
category: string[];
|
||||||
status: string[];
|
status: string[];
|
||||||
|
supplier_id?: number;
|
||||||
|
area_id?: number;
|
||||||
|
location_id?: number;
|
||||||
|
project_flock_id?: number;
|
||||||
|
project_flock_kandang_id?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user