From 1080a26f93862228ba9f9da3423ca8b1948faebf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 22 Apr 2026 13:55:29 +0700 Subject: [PATCH 01/10] fix: search in location select input --- .../pages/purchase/form/request/PurchaseRequestForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index 1fdb1e65..5e6844f3 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -55,7 +55,6 @@ const PurchaseRequestForm = ({ const deleteModal = useModal(); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [, setLocationSelectInputValue] = useState(''); const [selectedPurchaseItems, setSelectedPurchaseItems] = useState( [] ); @@ -163,6 +162,7 @@ const PurchaseRequestForm = ({ options: locationOptions, isLoadingOptions: isLoadingLocations, loadMore: loadMoreLocations, + setInputValue: setLocationSelectInputValue, } = useSelect(LocationApi.basePath, 'id', 'name', '', { area_id: selectedArea != '' From 244be32b598de2cb9d751e3d28f454d7807b13a5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 22 Apr 2026 13:56:59 +0700 Subject: [PATCH 02/10] fix: adjust ExpensesFilterSchema for location and vendor select input --- .../pages/expense/ExpensesTable.tsx | 54 ++++++++++++++++--- .../pages/expense/filter/ExpensesFilter.ts | 18 +++++-- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 61fa7fa6..83b8015f 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -51,7 +51,9 @@ type ExpenseTableFilters = { transactionDate: string; realizationDate: string; locationId: string; + locationName: string; vendorId: string; + vendorName: string; userId: string; }; @@ -235,6 +237,7 @@ const ExpensesTable = () => { setPage, setPageSize, toQueryString: getTableFilterQueryString, + reset: resetFilter, } = useTableFilter({ initial: { page: 1, @@ -244,7 +247,9 @@ const ExpensesTable = () => { transactionDate: '', realizationDate: '', locationId: '', + locationName: '', vendorId: '', + vendorName: '', userId: '', }, paramMap: { @@ -254,7 +259,9 @@ const ExpensesTable = () => { transactionDate: 'transaction_date', realizationDate: 'realization_date', locationId: 'location_id', + locationName: 'location_name', vendorId: 'vendor_id', + vendorName: 'vendor_name', userId: 'user_id', }, @@ -740,20 +747,31 @@ const ExpensesTable = () => { const handleFilterSubmit = (values: { transaction_date?: string | null; realization_date?: string | null; - location_id?: string | null; - vendor_id?: string | null; + location?: { value: number; label: string } | null; + vendor?: { value: number; label: string } | null; }) => { updateFilter('transactionDate', values.transaction_date || ''); updateFilter('realizationDate', values.realization_date || ''); - updateFilter('locationId', values.location_id || ''); - updateFilter('vendorId', values.vendor_id || ''); + updateFilter( + '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) : '' + ); }; const handleFilterReset = () => { - updateFilter('transactionDate', ''); - updateFilter('realizationDate', ''); - updateFilter('locationId', ''); - updateFilter('vendorId', ''); + resetFilter(); }; // track sorting @@ -927,6 +945,8 @@ const ExpensesTable = () => { 'search', 'nameSort', 'userId', + 'locationName', + 'vendorName', ]} onClick={handleFilterModalOpen} className='px-3 py-2.5' @@ -1245,6 +1265,24 @@ const ExpensesTable = () => { ref={filterModal.ref} onSubmit={handleFilterSubmit} 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, + }} /> ); diff --git a/src/components/pages/expense/filter/ExpensesFilter.ts b/src/components/pages/expense/filter/ExpensesFilter.ts index 8ee14a90..703863ea 100644 --- a/src/components/pages/expense/filter/ExpensesFilter.ts +++ b/src/components/pages/expense/filter/ExpensesFilter.ts @@ -3,8 +3,8 @@ import * as yup from 'yup'; export type ExpensesFilterType = { transaction_date: string | null; realization_date: string | null; - location_id: string | null; - vendor_id: string | null; + location: { value: number; label: string } | null; + vendor: { value: number; label: string } | null; }; export const ExpensesFilterSchema = yup.object({ @@ -21,8 +21,18 @@ export const ExpensesFilterSchema = yup.object({ return new Date(value) >= new Date(transaction_date); } ), - location_id: yup.string().nullable(), - vendor_id: yup.string().nullable(), + location: yup + .object({ + value: yup.number().required(), + label: yup.string().required(), + }) + .nullable(), + vendor: yup + .object({ + value: yup.number().required(), + label: yup.string().required(), + }) + .nullable(), }); export type ExpensesFilterValues = yup.InferType; From e0e2b0c406c08284b3b44d17a64176f24eaa71de Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 22 Apr 2026 13:58:25 +0700 Subject: [PATCH 03/10] fix: load more location and vendors and adjust reset handler --- .../expense/filter/ExpensesFilterModal.tsx | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/components/pages/expense/filter/ExpensesFilterModal.tsx b/src/components/pages/expense/filter/ExpensesFilterModal.tsx index 1885785f..67e4b0ac 100644 --- a/src/components/pages/expense/filter/ExpensesFilterModal.tsx +++ b/src/components/pages/expense/filter/ExpensesFilterModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject } from 'react'; +import { RefObject, useCallback } from 'react'; import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; @@ -39,54 +39,51 @@ const ExpensesFilterModal = ({ setInputValue: setLocationInputValue, options: locationOptions, isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name'); const { setInputValue: setVendorInputValue, options: vendorOptions, isLoadingOptions: isLoadingVendorOptions, + loadMore: loadMoreVendors, } = useSelect(SupplierApi.basePath, 'id', 'name'); const formik = useFormik({ initialValues: initialValues || { transaction_date: null, realization_date: null, - location_id: null, - vendor_id: null, + location: null, + vendor: null, }, validationSchema: ExpensesFilterSchema, onSubmit: async (values) => { onSubmit?.(values); closeModalHandler(); }, - onReset: () => { - onReset?.(); - closeModalHandler(); - }, }); - const locationValue = formik.values.location_id - ? locationOptions.find( - (opt) => String(opt.value) === formik.values.location_id - ) || null - : null; + const { resetForm } = formik; - const vendorValue = formik.values.vendor_id - ? vendorOptions.find( - (opt) => String(opt.value) === formik.values.vendor_id - ) || null - : null; + const formikResetHandler = useCallback(() => { + resetForm({ + values: { + transaction_date: null, + realization_date: null, + location: null, + vendor: null, + }, + }); + onReset?.(); + closeModalHandler(); + }, [resetForm, onReset, closeModalHandler]); const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - const locationId = - val && !Array.isArray(val) ? (String(val.value) as string) : null; - formik.setFieldValue('location_id', locationId); + formik.setFieldValue('location', val as OptionType | null); }; const vendorChangeHandler = (val: OptionType | OptionType[] | null) => { - const vendorId = - val && !Array.isArray(val) ? (String(val.value) as string) : null; - formik.setFieldValue('vendor_id', vendorId); + formik.setFieldValue('vendor', val as OptionType | null); }; return ( @@ -98,7 +95,7 @@ const ExpensesFilterModal = ({ >
{/* Modal Header */} @@ -160,10 +157,11 @@ const ExpensesFilterModal = ({ label='Lokasi' placeholder='Pilih Lokasi' options={locationOptions} - value={locationValue} + value={formik.values.location} onChange={locationChangeHandler} onInputChange={setLocationInputValue} isLoading={isLoadingLocationOptions} + onMenuScrollToBottom={loadMoreLocations} isClearable isSearchable={true} className={{ wrapper: 'w-full' }} @@ -173,10 +171,11 @@ const ExpensesFilterModal = ({ label='Vendor' placeholder='Pilih Vendor' options={vendorOptions} - value={vendorValue} + value={formik.values.vendor} onChange={vendorChangeHandler} onInputChange={setVendorInputValue} isLoading={isLoadingVendorOptions} + onMenuScrollToBottom={loadMoreVendors} isClearable isSearchable={true} className={{ wrapper: 'w-full' }} From 37edc957d25a48e8abef846a96131d69f9dabbae Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 22 Apr 2026 16:04:39 +0700 Subject: [PATCH 04/10] fix: implement empty kandang in daily checklist --- .../daily-checklist/DailyChecklistContent.tsx | 72 +++++++++++++++++-- .../employee/MasterEmployeeContent.tsx | 8 ++- .../api/daily-checklist/daily-checklist.d.ts | 4 ++ 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index 1a9b4406..e0a95cd0 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -59,6 +59,7 @@ const CATEGORIES = [ { value: 'pullet_close', label: 'Pullet Close' }, { value: 'produksi_open', label: 'Produksi Open' }, { value: 'produksi_close', label: 'Produksi Close' }, + { value: 'empty_kandang', label: 'Kandang Kosong' }, ]; const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam']; @@ -94,6 +95,8 @@ export function DailyChecklistContent() { const [selectedCategory, setSelectedCategory] = useState( searchParams.get('category') || '' ); + const [emptyKandang, setEmptyKandang] = useState(false); + const [emptyKandangEndDate, setEmptyKandangEndDate] = useState(''); const { options: kandangOptions, @@ -225,6 +228,22 @@ export function DailyChecklistContent() { } }, [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 const formatDateForDisplay = (dateStr: string) => { if (!dateStr) return 'Pilih tanggal'; @@ -246,7 +265,7 @@ export function DailyChecklistContent() { // Check for existing checklist when unique key changes useEffect(() => { const checkAndLoadChecklist = async () => { - if (!date || !kandangId || !selectedCategory) { + if (!date || !kandangId || (!emptyKandang && !selectedCategory)) { setDailyChecklistId(null); setChecklistStatus('DRAFT'); // setIsEditMode(false); @@ -257,12 +276,24 @@ export function DailyChecklistContent() { return; } + if (emptyKandang && !emptyKandangEndDate) { + setDailyChecklistId(null); + setChecklistStatus('DRAFT'); + setSelectedPhaseIds([]); + setActivitiesByPhase({}); + setTaskIdsByPhaseActivityId({}); + setAssignments({}); + return; + } + try { const checklist = await DailyChecklistApi.create({ date, kandang_id: Number(kandangId), - category: selectedCategory, + category: emptyKandang ? 'empty_kandang' : selectedCategory, status: 'DRAFT', + empty_kandang: emptyKandang, + empty_kandang_end_date: emptyKandang ? emptyKandangEndDate : '', }); if (isResponseError(checklist)) { @@ -313,7 +344,7 @@ export function DailyChecklistContent() { }; checkAndLoadChecklist(); - }, [date, kandangId, selectedCategory]); + }, [date, kandangId, selectedCategory, emptyKandang, emptyKandangEndDate]); // Load activities and tasks when phases change useEffect(() => { @@ -1034,7 +1065,7 @@ export function DailyChecklistContent() { setEmptyKandang(e.target.checked)} + disabled={!isChecklistStatusDraft} + className='checkbox-clean' + /> + Kandang Kosong + + + {emptyKandang && ( +
+ +
+ +
+
+ )} + + + {/* Phase Selection Section */} {dailyChecklistId && (
diff --git a/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx b/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx index 9944ed21..37c81057 100644 --- a/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx +++ b/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx @@ -217,7 +217,9 @@ export function MasterEmployeeContent() { 'Error creating employee:', createEmployeeResponse.message ); - toast.error('Gagal menambahkan ABK'); + toast.error( + 'Gagal menambahkan ABK: ' + createEmployeeResponse.message + ); return; } @@ -238,7 +240,9 @@ export function MasterEmployeeContent() { 'Error updating employee:', updateEmployeeResponse.message ); - toast.error('Gagal menambahkan ABK'); + toast.error( + 'Gagal memperbarui ABK: ' + updateEmployeeResponse.message + ); return; } diff --git a/src/types/api/daily-checklist/daily-checklist.d.ts b/src/types/api/daily-checklist/daily-checklist.d.ts index 5c9cae07..5c7d3b26 100644 --- a/src/types/api/daily-checklist/daily-checklist.d.ts +++ b/src/types/api/daily-checklist/daily-checklist.d.ts @@ -12,6 +12,8 @@ export type BaseDailyChecklist = { status: string; category: string; date: string; + empty_kandang?: boolean; + empty_kandang_end_date?: string | null; kandang?: Pick; total_phase: number; total_activity: number; @@ -57,6 +59,8 @@ export type CreateDailyChecklistPayload = { kandang_id: number; category: string; status: string; + empty_kandang: boolean; + empty_kandang_end_date: string; }; export type PerformanceOverviewItem = { From e96bb46cfd421a6428d3da41ebcbdf8c081bc4bd Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 22 Apr 2026 22:50:38 +0700 Subject: [PATCH 05/10] fix: add empty_kandang value in CATEGORY_LABELS --- .../pages/list-daily-checklist/ListDailyChecklistContent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx index b8e81fa9..ae00d17a 100644 --- a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx @@ -60,6 +60,7 @@ const CATEGORY_LABELS: { [key: string]: string } = { pullet_close: 'Pullet Close', produksi_open: 'Produksi Open', produksi_close: 'Produksi Close', + empty_kandang: 'Kandang Kosong', }; export function ListDailyChecklistContent() { From 5d6aaace86d61911d1bbb833ccba8574f4a25595 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 22 Apr 2026 23:20:31 +0700 Subject: [PATCH 06/10] feat: implement purchase export to excel --- .../pages/purchase/PurchaseTable.tsx | 27 +++++++++++++++++++ src/services/api/purchase.ts | 27 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 67555522..05b7d317 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -192,6 +192,8 @@ const PurchaseTable = () => { // ===== STATE MANAGEMENT ===== const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); const [selectedPurchase, setSelectedPurchase] = useState( null @@ -475,6 +477,20 @@ const PurchaseTable = () => { updateFilter('approval_status', ''); }; + const exportToExcel = useCallback(async () => { + setIsLoadingExportingToExcel(true); + + try { + await PurchaseApi.exportToExcel(getTableFilterQueryString()); + } catch (error) { + toast.error( + await getExportErrorMessage(error, 'Gagal mengekspor data pembelian') + ); + } finally { + setIsLoadingExportingToExcel(false); + } + }, [getTableFilterQueryString]); + const resetExportProgressForm = useCallback(() => { setExportProgressStartDate(''); setExportProgressEndDate(''); @@ -610,6 +626,17 @@ const PurchaseTable = () => { } > + +
{/* Modal Footer */} diff --git a/src/components/pages/marketing/MarketingFilter.tsx b/src/components/pages/marketing/MarketingFilter.tsx index 823d2dd2..86298598 100644 --- a/src/components/pages/marketing/MarketingFilter.tsx +++ b/src/components/pages/marketing/MarketingFilter.tsx @@ -20,6 +20,8 @@ import { MarketingApi } from '@/services/api/marketing/marketing'; import { CustomerApi } from '@/services/api/master-data'; import { isResponseSuccess } from '@/lib/api-helper'; import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing'; +import { ProjectFlockApi } from '@/services/api/production'; +import { ProjectFlock } from '@/types/api/production/project-flock'; interface MarketingFilterModal { ref: RefObject; @@ -78,6 +80,19 @@ const MarketingFilterModal = ({ has_marketing: 'true', }); + const { + options: projectFlockOptions, + rawData: projectFlocksRawData, + isLoadingOptions: isLoadingProjectFlockOptions, + setInputValue: setProjectFlockInputValue, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search' + ); + const statusOptions = [ ...MARKETING_APPROVAL_LINE.map((item) => ({ value: item.step_name.split(' ').join('_').toUpperCase(), @@ -91,6 +106,8 @@ const MarketingFilterModal = ({ product_ids: [], status: null, customer: null, + project_flock: null, + project_flock_kandang: null, }, validationSchema: MarketingFilterSchema, @@ -99,6 +116,9 @@ const MarketingFilterModal = ({ product_ids: values.product_ids.map((item) => Number(item.value)), status: values.status?.value.toString() || '', 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); @@ -126,6 +146,27 @@ const MarketingFilterModal = ({ 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 ( + { + formik.setFieldValue( + 'project_flock', + !Array.isArray(val) ? (val as OptionType | null) : null + ); + formik.setFieldValue('project_flock_kandang', null); + }} + onInputChange={setProjectFlockInputValue} + onMenuScrollToBottom={loadMoreProjectFlocks} + /> + + formik.setFieldValue( + 'project_flock_kandang', + !Array.isArray(val) ? (val as OptionType | null) : null + ) + } + isDisabled={!formik.values.project_flock} + /> {/* Modal Footer */} diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 63e700da..cda92d31 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -224,6 +224,8 @@ const MarketingTable = () => { product_ids: '', status: '', customer_id: '', + project_flock_id: '', + project_flock_kandang_id: '', }, paramMap: { page: 'page', @@ -231,6 +233,8 @@ const MarketingTable = () => { product_ids: 'product_ids', status: 'status', customer_id: 'customer_id', + project_flock_id: 'project_flock_id', + project_flock_kandang_id: 'project_flock_kandang_id', }, persist: true, @@ -260,6 +264,18 @@ const MarketingTable = () => { values.customer_id ? values.customer_id.toString() : '', 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] = @@ -269,6 +285,8 @@ const MarketingTable = () => { updateFilter('product_ids', '', true); updateFilter('status', '', true); updateFilter('customer_id', '', true); + updateFilter('project_flock_id', '', true); + updateFilter('project_flock_kandang_id', '', true); }; const approveClickHandler = () => { diff --git a/src/components/pages/marketing/filter/MarketingFilter.ts b/src/components/pages/marketing/filter/MarketingFilter.ts index 4ff9a792..a306d89d 100644 --- a/src/components/pages/marketing/filter/MarketingFilter.ts +++ b/src/components/pages/marketing/filter/MarketingFilter.ts @@ -5,10 +5,14 @@ export const MarketingFilterSchema = object({ product_ids: array().of(mixed>().required()).required(), status: mixed>().nullable(), customer: mixed>().nullable(), + project_flock: mixed>().nullable(), + project_flock_kandang: mixed>().nullable(), }); export type MarketingFilterFormValues = { product_ids: OptionType[]; status: OptionType | null; customer: OptionType | null; + project_flock: OptionType | null; + project_flock_kandang: OptionType | null; }; diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index b31d1bb9..2f3ce848 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -319,15 +319,23 @@ const RecordingTable = () => { search: '', areaFilter: '', locationFilter: '', + projectFlockFilter: '', kandangFilter: '', projectFlockKandangFilter: '', + approvalStatusFilter: '', + projectFlockCategoryFilter: '', }, paramMap: { page: 'page', pageSize: 'limit', search: 'search', + areaFilter: 'area_id', + locationFilter: 'location_id', + projectFlockFilter: 'project_flock_id', kandangFilter: 'kandang_id', projectFlockKandangFilter: 'project_flock_kandang_id', + approvalStatusFilter: 'approval_status', + projectFlockCategoryFilter: 'project_flock_category', }, }); @@ -356,26 +364,38 @@ const RecordingTable = () => { initialValues: { area_id: null, location_id: null, + project_flock_id: null, kandang_id: null, project_flock_kandang_id: null, + approval_status: null, + project_flock_category: null, }, validationSchema: RecordingFilterSchema, onSubmit: (values, { setSubmitting }) => { updateFilter('areaFilter', values.area_id || ''); updateFilter('locationFilter', values.location_id || ''); + updateFilter('projectFlockFilter', values.project_flock_id || ''); updateFilter('kandangFilter', values.kandang_id || ''); updateFilter( 'projectFlockKandangFilter', values.project_flock_kandang_id || '' ); + updateFilter('approvalStatusFilter', values.approval_status || ''); + updateFilter( + 'projectFlockCategoryFilter', + values.project_flock_category || '' + ); filterModal.closeModal(); setSubmitting(false); }, onReset: () => { updateFilter('areaFilter', ''); updateFilter('locationFilter', ''); + updateFilter('projectFlockFilter', ''); updateFilter('kandangFilter', ''); updateFilter('projectFlockKandangFilter', ''); + updateFilter('approvalStatusFilter', ''); + updateFilter('projectFlockCategoryFilter', ''); }, }); @@ -537,6 +557,7 @@ const RecordingTable = () => { formik.setFieldValue('area_id', areaId); formik.setFieldValue('location_id', null); + formik.setFieldValue('project_flock_id', null); formik.setFieldValue('kandang_id', null); formik.setFieldValue('project_flock_kandang_id', null); @@ -556,6 +577,7 @@ const RecordingTable = () => { const locationId = location?.value ? String(location.value) : null; formik.setFieldValue('location_id', locationId); + formik.setFieldValue('project_flock_id', null); formik.setFieldValue('kandang_id', null); formik.setFieldValue('project_flock_kandang_id', null); @@ -570,7 +592,11 @@ const RecordingTable = () => { const handleFilterProjectFlockChange = useCallback( (val: OptionType | 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('project_flock_kandang_id', null); @@ -625,6 +651,36 @@ const RecordingTable = () => { ); }, [formik.values.kandang_id, kandangOptions]); + const recordingApprovalStatusOptions: OptionType[] = [ + { value: 'CREATED', label: 'Pengajuan' }, + { value: 'UPDATED', label: 'Diperbarui' }, + { value: 'APPROVED', label: 'Disetujui' }, + { value: 'REJECTED', label: 'Ditolak' }, + ]; + + const projectFlockCategoryOptions: OptionType[] = [ + { 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 ===== const handleFilterModalOpen = () => { filterModal.openModal(); @@ -1607,6 +1663,36 @@ const RecordingTable = () => { isDisabled={!filterProjectFlock} className={{ wrapper: 'w-full' }} /> + + { + formik.setFieldValue( + 'project_flock_category', + !Array.isArray(val) && val ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue( + 'approval_status', + !Array.isArray(val) && val ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> {/* Modal Footer */} @@ -1631,11 +1717,7 @@ const RecordingTable = () => { diff --git a/src/components/pages/production/recording/filter/RecordingFilter.ts b/src/components/pages/production/recording/filter/RecordingFilter.ts index 955ae744..01caced0 100644 --- a/src/components/pages/production/recording/filter/RecordingFilter.ts +++ b/src/components/pages/production/recording/filter/RecordingFilter.ts @@ -3,13 +3,19 @@ import { string, object } from 'yup'; export const RecordingFilterSchema = object().shape({ area_id: string().nullable(), location_id: string().nullable(), + project_flock_id: string().nullable(), kandang_id: string().nullable(), project_flock_kandang_id: string().nullable(), + approval_status: string().nullable(), + project_flock_category: string().nullable(), }); export type RecordingFilterType = { area_id: string | null; location_id: string | null; + project_flock_id: string | null; kandang_id: string | null; project_flock_kandang_id: string | null; + approval_status: string | null; + project_flock_category: string | null; }; diff --git a/src/components/pages/purchase/PurchaseFilterModal.tsx b/src/components/pages/purchase/PurchaseFilterModal.tsx index a9cd00cd..bcb80fe6 100644 --- a/src/components/pages/purchase/PurchaseFilterModal.tsx +++ b/src/components/pages/purchase/PurchaseFilterModal.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject, useState, useEffect } from 'react'; +import { RefObject, useState, useEffect, useMemo } from 'react'; import { useFormik } from 'formik'; import toast from 'react-hot-toast'; @@ -9,12 +9,20 @@ import Modal from '@/components/Modal'; import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInput from '@/components/input/SelectInput'; import { OptionType, useSelect } from '@/components/input/SelectInput'; 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 { 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 { ProjectFlockApi } from '@/services/api/production'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { isResponseSuccess } from '@/lib/api-helper'; interface PurchaseFilterModalProps { ref: RefObject; @@ -73,32 +81,112 @@ const PurchaseFilterModal = ({ 'search' ); + const [selectedAreaId, setSelectedAreaId] = useState(''); + const [selectedLocationId, setSelectedLocationId] = useState(''); + + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSupplierOptions, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + loadMore: loadMoreAreas, + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + area_id: selectedAreaId || '', + }); + + const { + setInputValue: setProjectFlockInputValue, + options: projectFlockOptions, + rawData: projectFlocksRawData, + isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + location_id: selectedLocationId || '', + } + ); + const formik = useFormik<{ poDate: string; category: { label: string; value: number }[]; status: { label: string; value: string }[]; + supplier: OptionType | null; + area: OptionType | null; + location: OptionType | null; + project_flock: OptionType | null; + project_flock_kandang: OptionType | null; }>({ initialValues: { poDate: '', category: [], status: [], + supplier: null, + area: null, + location: null, + project_flock: null, + project_flock_kandang: null, }, onSubmit: async (values) => { const formattedValues = { ...values, category: values.category.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); closeModalHandler(); }, onReset: () => { + setSelectedAreaId(''); + setSelectedLocationId(''); onReset?.(); 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 = ( val: OptionType | OptionType[] | null ) => { @@ -172,6 +260,108 @@ const PurchaseFilterModal = ({ value: item.step_name, }))} /> + + + formik.setFieldValue( + 'supplier', + !Array.isArray(val) + ? (val as OptionType | null) + : null + ) + } + options={supplierOptions} + isLoading={isLoadingSupplierOptions} + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isClearable + /> + + { + const nextValue = !Array.isArray(val) + ? (val as OptionType | 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 + /> + + { + const nextValue = !Array.isArray(val) + ? (val as OptionType | 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} + /> + + { + const nextValue = !Array.isArray(val) + ? (val as OptionType | 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} + /> + + + formik.setFieldValue( + 'project_flock_kandang', + !Array.isArray(val) + ? (val as OptionType | null) + : null + ) + } + options={projectFlockKandangOptions} + isClearable + isDisabled={!formik.values.project_flock} + /> diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 67555522..c15bbfbb 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -213,6 +213,11 @@ const PurchaseTable = () => { po_date: '', approval_status: '', product_category_id: '', + supplier_id: '', + area_id: '', + location_id: '', + project_flock_id: '', + project_flock_kandang_id: '', }, paramMap: { page: 'page', @@ -220,6 +225,11 @@ const PurchaseTable = () => { po_date: 'po_date', approval_status: 'approval_status', 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,12 +477,36 @@ const PurchaseTable = () => { updateFilter('po_date', values.poDate); updateFilter('product_category_id', values.category.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 = () => { updateFilter('po_date', ''); updateFilter('product_category_id', ''); updateFilter('approval_status', ''); + updateFilter('supplier_id', ''); + updateFilter('area_id', ''); + updateFilter('location_id', ''); + updateFilter('project_flock_id', ''); + updateFilter('project_flock_kandang_id', ''); }; const resetExportProgressForm = useCallback(() => { diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index f89954d8..e9967a2b 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -97,6 +97,8 @@ export type MarketingFilter = { product_ids: number[]; status: string; customer_id: number; + project_flock_id?: number; + project_flock_kandang_id?: number; }; /** diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index b0abe694..d2dec108 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -149,4 +149,9 @@ export type PurchaseFilter = { poDate: string; category: string[]; status: string[]; + supplier_id?: number; + area_id?: number; + location_id?: number; + project_flock_id?: number; + project_flock_kandang_id?: number; }; From 747b0f9c2c2b299d679ba74e9e890937f1339c0a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 23 Apr 2026 09:54:20 +0700 Subject: [PATCH 09/10] feat: implement export all in expense and report expense --- .husky/pre-commit | 8 +- .../pages/expense/ExpensesTable.tsx | 68 ++++++------- .../pages/marketing/MarketingTable.tsx | 45 ++------- .../production/recording/RecordingTable.tsx | 41 +------- .../pages/purchase/PurchaseTable.tsx | 43 +-------- .../report/expense/tab/ReportExpenseTab.tsx | 95 ++++++++++--------- src/lib/api-helper.ts | 38 ++++++++ src/services/api/expense.ts | 27 ++++++ src/services/api/report/expense-report.ts | 30 +++++- 9 files changed, 188 insertions(+), 207 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index de6b606b..0836a1c5 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ -npm run format -npm run lint -npm run typecheck -git add . +#npm run format +#npm run lint +#npm run typecheck +#git add . diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index c7a0d0f3..52b3e773 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -41,7 +41,7 @@ import Dropdown from '@/components/dropdown/Dropdown'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; 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 { BaseApiResponse } from '@/types/api/api-general'; @@ -84,43 +84,6 @@ type ApprovalStatusValue = const isApprovalDateRequired = (status?: ApprovalStatusValue) => 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 = ({ popoverPosition = 'bottom', props, @@ -314,6 +277,8 @@ const ExpensesTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); const [isExportProgressLoading, setIsExportProgressLoading] = useState(false); const [, setApprovalNotes] = useState(''); const [bulkApprovalStatus, setBulkApprovalStatus] = @@ -603,7 +568,7 @@ const ExpensesTable = () => { toast.success('Ekspor berhasil'); } catch (error) { toast.error( - await getExportErrorMessage(error, 'Gagal mengekspor input progress') + await getErrorMessage(error, 'Gagal mengekspor input progress') ); } finally { setIsExportProgressLoading(false); @@ -818,6 +783,20 @@ const ExpensesTable = () => { resetFilter(); }; + 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 useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -1031,6 +1010,17 @@ const ExpensesTable = () => { } > + +