From 244d800874505b062a43f57eb5a1247532f6bf29 Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Tue, 14 Apr 2026 13:10:53 +0700 Subject: [PATCH 01/32] codex/fix: uniformity week calculation --- .../uniformity/form/UniformityForm.schema.ts | 9 +-- .../uniformity/form/UniformityForm.tsx | 73 ------------------- .../uniformity/form/UniformityResultForm.tsx | 1 - src/services/api/uniformity.ts | 1 - src/types/api/production/uniformity.d.ts | 1 - 5 files changed, 1 insertion(+), 84 deletions(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.schema.ts b/src/components/pages/production/uniformity/form/UniformityForm.schema.ts index 037180f0..ad2f1e61 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.schema.ts +++ b/src/components/pages/production/uniformity/form/UniformityForm.schema.ts @@ -3,7 +3,6 @@ import { Uniformity } from '@/types/api/production/uniformity'; type UniformityFormSchemaType = { date: string; - week: number; location?: { value: number; label: string; @@ -45,10 +44,6 @@ const FileSchema = Yup.mixed() export const UniformityFormSchema: Yup.ObjectSchema = Yup.object({ date: Yup.string().required('Tanggal wajib diisi!'), - week: Yup.number() - .min(1, 'Minggu ke wajib diisi!') - .required('Minggu ke wajib diisi!') - .typeError('Minggu ke wajib diisi!'), location: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), @@ -81,7 +76,6 @@ export type UniformityFormValues = Yup.InferType; export type UniformityFormData = { date: string; - week: number; project_flock_kandang_id: number; document: File | null; document_name: string; @@ -91,8 +85,7 @@ export const getUniformityFormInitialValues = ( initialValues?: Partial ): UniformityFormValues => { return { - date: initialValues?.week ? '' : '', - week: initialValues?.week ?? 0, + date: '', location: null, location_id: 0, project_flock: null, diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 80668748..46c97278 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -27,7 +27,6 @@ import { LocationApi } from '@/services/api/master-data'; import { ProjectFlockApi, ProjectFlockKandangApi, - RecordingApi, } from '@/services/api/production'; import { UniformityApi } from '@/services/api/uniformity'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; @@ -40,7 +39,6 @@ import { ProjectFlockKandangLookup, ProjectFlock, } from '@/types/api/production/project-flock'; -import { Recording } from '@/types/api/production/recording'; import { Kandang } from '@/types/api/master-data/kandang'; import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm'; import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm'; @@ -204,23 +202,6 @@ const UniformityForm = ({ ? projectFlockKandangLookupData.data : undefined; - // ===== RECORDINGS DATA (FOR WEEK CALCULATION) ===== - const recordingsUrl = useMemo(() => { - if (!projectFlockKandangLookup?.project_flock_kandang_id) return null; - const params = new URLSearchParams({ - page: '1', - limit: '100', - project_flock_kandang_id: - projectFlockKandangLookup.project_flock_kandang_id.toString(), - }); - return `${RecordingApi.basePath}?${params.toString()}`; - }, [projectFlockKandangLookup?.project_flock_kandang_id]); - - const { data: recordingsData } = useSWR( - recordingsUrl, - recordingsUrl ? RecordingApi.getAllFetcher : null - ); - // ===== FORM CONFIGURATION ===== const formikInitialValues = useMemo( () => getUniformityFormInitialValues(initialValues), @@ -246,7 +227,6 @@ const UniformityForm = ({ setUniformityFormData({ date: values.date, - week: values.week, project_flock_kandang_id: projectFlockKandangId, document: values.document as File, document_name: (values.document as File).name, @@ -475,59 +455,6 @@ const UniformityForm = ({ generateUniformityTemplate(population, projectFlockKandangLookup); }, [projectFlockKandangLookup]); - // ===== SIDE EFFECTS ===== - useEffect(() => { - if ( - projectFlockKandangLookup?.chick_in_date && - projectFlockKandangLookup?.project_flock_kandang_id - ) { - const chickInDate = new Date(projectFlockKandangLookup.chick_in_date); - chickInDate.setHours(0, 0, 0, 0); - - let initialWeek = 18; - - if ( - isResponseSuccess(recordingsData) && - recordingsData.data && - recordingsData.data.length > 0 - ) { - const sortedRecordings = [...recordingsData.data].sort( - (a: Recording, b: Recording) => - new Date(a.record_datetime).getTime() - - new Date(b.record_datetime).getTime() - ); - - const earliestRecording = sortedRecordings[0]; - if (earliestRecording?.project_flock?.production_standart?.week) { - initialWeek = - earliestRecording.project_flock.production_standart.week; - } - } - - if (formik.values.date) { - const selectedDate = new Date(formik.values.date); - selectedDate.setHours(0, 0, 0, 0); - - const daysDiff = Math.floor( - (selectedDate.getTime() - chickInDate.getTime()) / - (1000 * 60 * 60 * 24) - ); - - const weeksDiff = Math.floor(daysDiff / 7); - - setFieldValue('week', initialWeek + weeksDiff); - } else { - setFieldValue('week', initialWeek); - } - } - }, [ - projectFlockKandangLookup?.chick_in_date, - projectFlockKandangLookup?.project_flock_kandang_id, - recordingsData, - formik.values.date, - setFieldValue, - ]); - useEffect(() => { const unsub = subscribeValidate(() => { setIsValid(true); diff --git a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx index 108cb4f8..a5dda1e4 100644 --- a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx @@ -63,7 +63,6 @@ const UniformityResultForm = () => { try { const payload = { date: uniformityFormData.date, - week: uniformityFormData.week, project_flock_kandang_id: uniformityFormData.project_flock_kandang_id, document: uniformityFormData.document, }; diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 7ed8098e..dda35787 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -56,7 +56,6 @@ export class UniformityApiService extends BaseApiService< ): Promise | undefined> { const formData = new FormData(); formData.append('date', payload.date); - formData.append('week', payload.week.toString()); formData.append( 'project_flock_kandang_id', payload.project_flock_kandang_id.toString() diff --git a/src/types/api/production/uniformity.d.ts b/src/types/api/production/uniformity.d.ts index 825607d8..b4ff2ed8 100644 --- a/src/types/api/production/uniformity.d.ts +++ b/src/types/api/production/uniformity.d.ts @@ -146,7 +146,6 @@ export type CreateUniformityPayload = { date: string; project_flock_kandang_id: number; document: File; - week: number; }; export type VerifyUniformityPayload = { From 8dc62453bdf8d56653f01e602b59d2fc6b02cfaa Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 14 Apr 2026 13:31:40 +0700 Subject: [PATCH 02/32] fix(FE-form-object-missmatch): Refactor purchase item handling in approval forms and schemas --- .../order/PurchaseOrderAcceptApprovalForm.tsx | 3 +-- .../form/order/PurchaseOrderForm.schema.ts | 25 ++++--------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 2eacfbad..7ab0dc81 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -294,7 +294,6 @@ const PurchaseOrderAcceptApprovalForm = ({ item.expedition_vendor_id || item.expedition_vendor?.id || null; return { - purchase_item: null, purchase_item_id: item.id, received_date: item.received_date ? new Date(item.received_date).toISOString().split('T')[0] @@ -573,7 +572,7 @@ const PurchaseOrderAcceptApprovalForm = ({ expeditionVendorChangeHandler(idx, val) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index 9e6f43b0..aac4fa50 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -31,10 +31,6 @@ type PurchaseRequestAcceptApprovalFormSchemaType = { action: 'APPROVED' | 'REJECTED'; notes: string | null; items: { - purchase_item?: { - value: number; - label: string; - } | null; purchase_item_id: number; received_date: string; travel_number: string; @@ -68,10 +64,6 @@ export type PurchaseStaffApprovalItemSchema = { }; export type PurchaseAcceptApprovalItemSchema = { - purchase_item?: { - value: number; - label: string; - } | null; purchase_item_id: number; received_date: string; travel_number: string; @@ -160,12 +152,6 @@ const PurchaseManagerApprovalObjectSchema: Yup.ObjectSchema = Yup.object({ - purchase_item: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }) - .nullable() - .optional(), purchase_item_id: Yup.number() .min(1, 'Purchase item is required!') .required('Purchase item is required!') @@ -185,9 +171,8 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema - Boolean(expeditionVendor?.value), + .when('expedition_vendor_id', { + is: (expeditionVendorId?: number | null) => Boolean(expeditionVendorId), then: (schema) => schema.required('Nomor kendaraan wajib diisi!'), otherwise: (schema) => schema.optional(), }) @@ -196,6 +181,7 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema() .nullable() - .when('expedition_vendor', { - is: (expeditionVendor?: { value?: number; label?: string } | null) => - Boolean(expeditionVendor?.value), + .when('expedition_vendor_id', { + is: (expeditionVendorId?: number | null) => Boolean(expeditionVendorId), then: (schema) => schema.required('Biaya transport per item wajib diisi!'), otherwise: (schema) => schema.optional(), From 2a33fdbbbe24ca2d403d9662baeff4c060c082f2 Mon Sep 17 00:00:00 2001 From: MacBook Air M1 Date: Tue, 14 Apr 2026 15:05:08 +0700 Subject: [PATCH 03/32] adjust value query param get product warehouses --- .../pages/production/recording/form/RecordingForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 2a044874..5ffa1344 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -611,7 +611,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { flags: 'PAKAN,OVK', limit: '100', - available_only: 'true', + available_only: 'false', location_id: stockProductsLocationId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), }); From 5e907d7e53a24f67567d393fc5a3dc708f1ecad3 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 15 Apr 2026 16:35:35 +0700 Subject: [PATCH 04/32] feat: create expense navigation helper function --- src/lib/expense-list-navigation.ts | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/lib/expense-list-navigation.ts diff --git a/src/lib/expense-list-navigation.ts b/src/lib/expense-list-navigation.ts new file mode 100644 index 00000000..a9f24539 --- /dev/null +++ b/src/lib/expense-list-navigation.ts @@ -0,0 +1,39 @@ +type SearchParamsLike = { + get: (name: string) => string | null; +}; + +const EXPENSE_LIST_PATH = '/expense'; + +export const getExpenseListReturnTo = (searchParams: SearchParamsLike) => { + const existingReturnTo = searchParams.get('returnTo'); + + if (existingReturnTo?.startsWith(EXPENSE_LIST_PATH)) { + return existingReturnTo; + } + + const params = new URLSearchParams(); + const page = searchParams.get('page'); + const limit = searchParams.get('limit'); + + if (page) params.set('page', page); + if (limit) params.set('limit', limit); + + const queryString = params.toString(); + + return queryString + ? `${EXPENSE_LIST_PATH}?${queryString}` + : EXPENSE_LIST_PATH; +}; + +export const buildExpenseActionHref = ( + path: string, + expenseId: number | string, + searchParams: SearchParamsLike +) => { + const params = new URLSearchParams({ + expenseId: String(expenseId), + returnTo: getExpenseListReturnTo(searchParams), + }); + + return `${path}?${params.toString()}`; +}; From 7a5ee2aca1f4c6c31811c29a9b04715bb3f8ef11 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 15 Apr 2026 16:38:56 +0700 Subject: [PATCH 05/32] feat: implement return to url query param --- .../pages/expense/ExpenseDetail.tsx | 6 +- .../expense/ExpenseRealizationContent.tsx | 12 +- .../pages/expense/ExpenseRequestContent.tsx | 28 +++-- .../pages/expense/ExpensesTable.tsx | 108 +++++++++++++++--- .../expense/form/ExpenseRealizationForm.tsx | 15 ++- 5 files changed, 140 insertions(+), 29 deletions(-) diff --git a/src/components/pages/expense/ExpenseDetail.tsx b/src/components/pages/expense/ExpenseDetail.tsx index 1f43eae1..c09b168a 100644 --- a/src/components/pages/expense/ExpenseDetail.tsx +++ b/src/components/pages/expense/ExpenseDetail.tsx @@ -1,6 +1,7 @@ 'use client'; import { useMemo, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -9,6 +10,7 @@ import ExpenseRequestContent from '@/components/pages/expense/ExpenseRequestCont import ExpenseRealizationContent from '@/components/pages/expense/ExpenseRealizationContent'; import { Expense } from '@/types/api/expense'; +import { getExpenseListReturnTo } from '@/lib/expense-list-navigation'; interface ExpenseDetailProps { initialValues?: Expense; @@ -16,6 +18,8 @@ interface ExpenseDetailProps { const ExpenseDetail: React.FC = ({ initialValues }) => { const [activeTab, setActiveTab] = useState('request'); + const searchParams = useSearchParams(); + const returnTo = getExpenseListReturnTo(searchParams); const expenseDetailTabs = useMemo(() => { const validTabs = [ @@ -46,7 +50,7 @@ const ExpenseDetail: React.FC = ({ initialValues }) => {
+ } + > + + + + + ), + [filterParams, handleExport] + ); + + useEffect(() => { + setTabActions(tabId, tabActionsElement); + }, [setTabActions, tabActionsElement, tabId]); + + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [clearTabActions, tabId]); + + const columns = useMemo((): ColumnDef[] => { + return [ + { + header: 'Flock', + accessorKey: 'flock', + }, + { + header: 'Total Cost Pullet', + accessorKey: 'totalCostPullet', + cell: ({ row }) => formatCurrency(row.original.totalCostPullet), + }, + { + header: 'Total Depresiasi', + accessorKey: 'totalDepresiasi', + cell: ({ row }) => formatCurrency(row.original.totalDepresiasi), + }, + { + header: 'Periode', + accessorKey: 'periode', + }, + { + header: 'Farm', + accessorKey: 'farm', + }, + { + header: 'Jumlah Kandang', + accessorKey: 'jumlahKandang', + cell: ({ row }) => row.original.jumlahKandang.toLocaleString('id-ID'), + }, + ]; + }, []); + + return ( + <> +
+ {filteredData.length === 0 && ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + )} + + {filteredData.length > 0 && ( + <> + + +
+ + setPage((currentPage) => + currentPage > 1 ? currentPage - 1 : currentPage + ) + } + onNextPage={() => + setPage((currentPage) => + currentPage < totalPages ? currentPage + 1 : currentPage + ) + } + onPageChange={setPage} + rowOptions={[10, 20, 50, 100]} + onRowChange={(value) => { + setPageSize(value); + setPage(1); + }} + /> +
+ + )} + + + +
+
+ +

Filter Data

+
+ +
+ +
+
+ + setDraftFilters((current) => ({ + ...current, + farm: (val as OptionType | null)?.value?.toString() || null, + })) + } + isClearable + className={{ wrapper: 'w-full' }} + /> + + + setDraftFilters((current) => ({ + ...current, + period: (val as OptionType | null)?.value?.toString() || null, + })) + } + isClearable + className={{ wrapper: 'w-full' }} + /> +
+ +
+ + +
+ +
+ + ); +}; + +const filterToOption = ( + options: OptionType[], + value: string | null +): OptionType | null => { + if (!value) return null; + return options.find((option) => option.value === value) || null; +}; + +export default ReportDepreciationTab; From 16741aaa46ace4ffcdd17d3f820b25dfa780fb27 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 17 Apr 2026 13:27:21 +0700 Subject: [PATCH 11/32] feat: create ReportDepreciation and ReportDepreciationSearchParams type --- src/types/api/report/report-expense.d.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/types/api/report/report-expense.d.ts b/src/types/api/report/report-expense.d.ts index bf9b94eb..f513b249 100644 --- a/src/types/api/report/report-expense.d.ts +++ b/src/types/api/report/report-expense.d.ts @@ -52,3 +52,18 @@ export type ReportExpenseSearchParams = { category: string | null; search: string; }; + +export type ReportDepreciation = { + id: string; + flock: string; + totalCostPullet: number; + totalDepresiasi: number; + periode: string; + farm: string; + jumlahKandang: number; +}; + +export type ReportDepreciationSearchParams = { + farm: string | null; + period: string | null; +}; From 08aa79a06b7f4b911aab6ae616bc3d765eaa6286 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 17 Apr 2026 14:07:52 +0700 Subject: [PATCH 12/32] fix: adjust limit to get all recording data in exportToExcel method --- src/services/api/production.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/api/production.ts b/src/services/api/production.ts index 1f2a0373..5009b261 100644 --- a/src/services/api/production.ts +++ b/src/services/api/production.ts @@ -95,6 +95,8 @@ export class RecordingService extends BaseApiService< const params = new URLSearchParams(initialQueryString); params.set('export', 'excel'); + params.set('page', '1'); + params.set('limit', '99999999999'); const queryString = `?${params.toString()}`; From 8afc1a63818fcd2bbdd01983017331894e94e612 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 20 Apr 2026 00:28:41 +0700 Subject: [PATCH 13/32] feat: create ReportDepreciationFilterModal component --- .../tab/ReportDepreciationFilterModal.tsx | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx diff --git a/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx new file mode 100644 index 00000000..91e5f536 --- /dev/null +++ b/src/components/pages/report/expense/tab/ReportDepreciationFilterModal.tsx @@ -0,0 +1,270 @@ +'use client'; + +import { RefObject, useMemo, useState } from 'react'; +import { useFormik } from 'formik'; +import * as yup from 'yup'; + +import { Icon } from '@iconify/react'; +import Modal from '@/components/Modal'; +import Button from '@/components/Button'; +import DateInput from '@/components/input/DateInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; + +import { AreaApi, LocationApi } from '@/services/api/master-data'; +import { ProjectFlockApi } from '@/services/api/production'; +import { Area } from '@/types/api/master-data/area'; +import { Location } from '@/types/api/master-data/location'; +import { ProjectFlock } from '@/types/api/production/project-flock'; + +export type ReportDepreciationFilterValues = { + area_id: string | null; + location_id: string | null; + project_flock_id: string | null; + period: string | null; +}; + +export const ReportDepreciationFilterSchema = yup.object({ + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + project_flock_id: yup.string().nullable(), + period: yup.string().nullable().required('Periode wajib dipilih'), +}) as yup.ObjectSchema; + +interface ReportDepreciationFilterModalProps { + ref: RefObject; + initialValues?: ReportDepreciationFilterValues; + onSubmit?: (values: Partial) => void; + onReset?: () => void; +} + +const defaultInitialValues: ReportDepreciationFilterValues = { + area_id: null, + location_id: null, + project_flock_id: null, + period: null, +}; + +const ReportDepreciationFilterModal = ({ + ref, + initialValues, + onSubmit, + onReset, +}: ReportDepreciationFilterModalProps) => { + const [selectedAreaId, setSelectedAreaId] = useState( + initialValues?.area_id || undefined + ); + const [selectedLocationId, setSelectedLocationId] = useState< + string | undefined + >(initialValues?.location_id || undefined); + + const closeModalHandler = () => { + ref.current?.close(); + }; + + 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, + isLoadingOptions: isLoadingProjectFlockOptions, + loadMore: loadMoreProjectFlocks, + } = useSelect( + ProjectFlockApi.basePath, + 'id', + 'flock_name', + 'search', + { + location_id: selectedLocationId || '', + } + ); + + const formik = useFormik({ + initialValues: initialValues || defaultInitialValues, + validationSchema: ReportDepreciationFilterSchema, + onSubmit: async (values) => { + onSubmit?.(values); + closeModalHandler(); + }, + onReset: (_) => { + onReset?.(); + closeModalHandler(); + }, + }); + + const areaValue = useMemo(() => { + if (!formik.values.area_id) return null; + return ( + areaOptions.find((opt) => String(opt.value) === formik.values.area_id) || + null + ); + }, [formik.values.area_id, areaOptions]); + + const locationValue = useMemo(() => { + if (!formik.values.location_id) return null; + return ( + locationOptions.find( + (opt) => String(opt.value) === formik.values.location_id + ) || null + ); + }, [formik.values.location_id, locationOptions]); + + const projectFlockValue = useMemo(() => { + if (!formik.values.project_flock_id) return null; + return ( + projectFlockOptions.find( + (opt) => String(opt.value) === formik.values.project_flock_id + ) || null + ); + }, [formik.values.project_flock_id, projectFlockOptions]); + + const areaChangeHandler = (val: OptionType | OptionType[] | null) => { + const areaId = val && !Array.isArray(val) ? String(val.value) : null; + + setSelectedAreaId(areaId || undefined); + formik.setFieldValue('area_id', areaId); + formik.setFieldValue('location_id', null); + formik.setFieldValue('project_flock_id', null); + setSelectedLocationId(undefined); + }; + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + const locationId = val && !Array.isArray(val) ? String(val.value) : null; + + setSelectedLocationId(locationId || undefined); + formik.setFieldValue('location_id', locationId); + formik.setFieldValue('project_flock_id', null); + }; + + const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { + const projectFlockId = + val && !Array.isArray(val) ? String(val.value) : null; + + formik.setFieldValue('project_flock_id', projectFlockId); + }; + + return ( + +
+
+
+ +

Filter Data

+
+ + +
+ +
+ + + + + + + +
+ +
+ + + +
+ +
+ ); +}; + +export default ReportDepreciationFilterModal; From 2ca733de97b93968ea4dadd4befd5ed44b152db2 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 20 Apr 2026 00:29:15 +0700 Subject: [PATCH 14/32] fix: adjust ReportDepreciationTab content --- .../expense/tab/ReportDepreciationTab.tsx | 584 ++++++------------ 1 file changed, 187 insertions(+), 397 deletions(-) diff --git a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx index 403bc63b..8c6caaa3 100644 --- a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx +++ b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx @@ -1,271 +1,121 @@ 'use client'; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { Icon } from '@iconify/react'; +import React, { useEffect, useMemo } from 'react'; +import useSWR from 'swr'; import { ColumnDef } from '@tanstack/react-table'; -import Button from '@/components/Button'; -import Dropdown from '@/components/dropdown/Dropdown'; -import Modal, { useModal } from '@/components/Modal'; + +import { Icon } from '@iconify/react'; +import Card from '@/components/Card'; import Pagination from '@/components/Pagination'; import Table from '@/components/Table'; import ButtonFilter from '@/components/helper/ButtonFilter'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton'; +import { useModal } from '@/components/Modal'; +import ReportDepreciationFilterModal from '@/components/pages/report/expense/tab/ReportDepreciationFilterModal'; + import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; -import { formatCurrency } from '@/lib/helper'; -import { - ReportDepreciation, - ReportDepreciationSearchParams, -} from '@/types/api/report/report-expense'; -import toast from 'react-hot-toast'; +import { ReportDepreciation } from '@/types/api/report/report-expense'; +import { DepreciationReportApi } from '@/services/api/report/expense-report'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; interface ReportDepreciationTabProps { tabId: string; } -const DUMMY_DEPRECIATION_DATA: ReportDepreciation[] = [ - { - id: 'DEP-001', - flock: 'Flock A-01', - totalCostPullet: 185000000, - totalDepresiasi: 38500000, - periode: 'Periode 1', - farm: 'Farm Sukamaju', - jumlahKandang: 4, - }, - { - id: 'DEP-002', - flock: 'Flock A-02', - totalCostPullet: 192500000, - totalDepresiasi: 40125000, - periode: 'Periode 1', - farm: 'Farm Sukamaju', - jumlahKandang: 5, - }, - { - id: 'DEP-003', - flock: 'Flock B-01', - totalCostPullet: 176800000, - totalDepresiasi: 36200000, - periode: 'Periode 2', - farm: 'Farm Cibitung', - jumlahKandang: 3, - }, - { - id: 'DEP-004', - flock: 'Flock B-02', - totalCostPullet: 201400000, - totalDepresiasi: 42250000, - periode: 'Periode 2', - farm: 'Farm Cibitung', - jumlahKandang: 4, - }, - { - id: 'DEP-005', - flock: 'Flock C-01', - totalCostPullet: 210900000, - totalDepresiasi: 43950000, - periode: 'Periode 3', - farm: 'Farm Karawang', - jumlahKandang: 6, - }, - { - id: 'DEP-006', - flock: 'Flock C-02', - totalCostPullet: 205750000, - totalDepresiasi: 43100000, - periode: 'Periode 3', - farm: 'Farm Karawang', - jumlahKandang: 5, - }, - { - id: 'DEP-007', - flock: 'Flock D-01', - totalCostPullet: 188300000, - totalDepresiasi: 39000000, - periode: 'Periode 4', - farm: 'Farm Subang', - jumlahKandang: 4, - }, - { - id: 'DEP-008', - flock: 'Flock D-02', - totalCostPullet: 197600000, - totalDepresiasi: 40875000, - periode: 'Periode 4', - farm: 'Farm Subang', - jumlahKandang: 5, - }, -]; - -const INITIAL_FILTERS: ReportDepreciationSearchParams = { - farm: null, - period: null, -}; - const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { - const [filterParams, setFilterParams] = - useState(INITIAL_FILTERS); - const [draftFilters, setDraftFilters] = - useState(INITIAL_FILTERS); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + reset: resetFilter, + } = useTableFilter({ + initial: { + area_id: '', + location_id: '', + project_flock_id: '', + period: formatDate(Date.now(), 'YYYY-MM-DD'), + }, + paramMap: { + pageSize: 'limit', + area_id: 'area_id', + location_id: 'location_id', + project_flock_id: 'project_flock_id', + period: 'period', + }, + }); + + const { data: depreciationsResponse, isLoading: isLoadingDepreciations } = + useSWR( + `${DepreciationReportApi.basePath}${getTableFilterQueryString()}`, + DepreciationReportApi.getAllFetcher + ); + + const depreciations = isResponseSuccess(depreciationsResponse) + ? depreciationsResponse.data + : []; - const handleFilterModalOpenRef = useRef(() => {}); const filterModal = useModal(); + const { ref: filterModalRef } = filterModal; + const setTabActions = useTabActionsStore((state) => state.setTabActions); const clearTabActions = useTabActionsStore((state) => state.clearTabActions); - const farmOptions = useMemo( - () => - Array.from(new Set(DUMMY_DEPRECIATION_DATA.map((item) => item.farm))).map( - (farm) => ({ - value: farm, - label: farm, - }) - ), - [] - ); - - const periodOptions = useMemo( - () => - Array.from( - new Set(DUMMY_DEPRECIATION_DATA.map((item) => item.periode)) - ).map((period) => ({ - value: period, - label: period, - })), - [] - ); - - const draftFarmValue = useMemo( - () => filterToOption(farmOptions, draftFilters.farm), - [draftFilters.farm, farmOptions] - ); - - const draftPeriodValue = useMemo( - () => filterToOption(periodOptions, draftFilters.period), - [draftFilters.period, periodOptions] - ); - - const filteredData = useMemo(() => { - return DUMMY_DEPRECIATION_DATA.filter((item) => { - const matchFarm = filterParams.farm - ? item.farm === filterParams.farm - : true; - const matchPeriod = filterParams.period - ? item.periode === filterParams.period - : true; - - return matchFarm && matchPeriod; - }); - }, [filterParams]); - - const totalPages = useMemo( - () => Math.max(1, Math.ceil(filteredData.length / pageSize)), - [filteredData.length, pageSize] - ); - - useEffect(() => { - if (page > totalPages) { - setPage(totalPages); - } - }, [page, totalPages]); - - const paginatedData = useMemo(() => { - const startIndex = (page - 1) * pageSize; - return filteredData.slice(startIndex, startIndex + pageSize); - }, [filteredData, page, pageSize]); - - handleFilterModalOpenRef.current = () => { - setDraftFilters(filterParams); - filterModal.openModal(); - }; - - const handleApplyFilters = (e: React.FormEvent) => { - e.preventDefault(); - setFilterParams(draftFilters); - setPage(1); - filterModal.closeModal(); - }; - - const handleResetFilters = () => { - setDraftFilters(INITIAL_FILTERS); - setFilterParams(INITIAL_FILTERS); - setPage(1); - filterModal.closeModal(); - }; - - const handleExport = useCallback((type: 'excel' | 'pdf') => { - toast.success( - `Export ${type.toUpperCase()} belum terhubung API. Saat ini tabel memakai dummy data.` - ); - }, []); + const depreciationKandangColumns: ColumnDef< + ReportDepreciation['components']['kandang'][0] + >[] = [ + { + accessorKey: 'kandang_name', + header: 'Kandang', + }, + { + accessorKey: 'house_type', + header: 'Tipe Kandang', + cell: ({ row }) => row.original.house_type.toUpperCase(), + }, + { + accessorKey: 'depreciation_percent', + header: 'Persentase Depresiasi', + cell: ({ row }) => row.original.depreciation_percent + '%', + }, + { + accessorKey: 'depreciation_value', + header: 'Nilai Depresiasi', + cell: ({ row }) => formatCurrency(row.original.depreciation_value), + }, + { + accessorKey: 'depreciation_source', + header: 'Asal Depresiasi', + cell: ({ row }) => row.original.depreciation_source.toUpperCase(), + }, + { + accessorKey: 'cutover_date', + header: 'Tanggal Cutover', + cell: ({ row }) => formatDate(row.original.cutover_date, 'DD MMM YYYY'), + }, + { + accessorKey: 'origin_date', + header: 'Tanggal Origin', + cell: ({ row }) => formatDate(row.original.origin_date, 'DD MMM YYYY'), + }, + ]; const tabActionsElement = useMemo( () => (
handleFilterModalOpenRef.current()} + values={tableFilterState} + excludeFields={['page', 'pageSize']} + onClick={() => filterModal.openModal()} variant='outline' className='px-3 py-2.5' /> - - -
- - Export -
- -
- - } - > - - -
), - [filterParams, handleExport] + [tableFilterState] ); useEffect(() => { @@ -278,44 +128,18 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { }; }, [clearTabActions, tabId]); - const columns = useMemo((): ColumnDef[] => { - return [ - { - header: 'Flock', - accessorKey: 'flock', - }, - { - header: 'Total Cost Pullet', - accessorKey: 'totalCostPullet', - cell: ({ row }) => formatCurrency(row.original.totalCostPullet), - }, - { - header: 'Total Depresiasi', - accessorKey: 'totalDepresiasi', - cell: ({ row }) => formatCurrency(row.original.totalDepresiasi), - }, - { - header: 'Periode', - accessorKey: 'periode', - }, - { - header: 'Farm', - accessorKey: 'farm', - }, - { - header: 'Jumlah Kandang', - accessorKey: 'jumlahKandang', - cell: ({ row }) => row.original.jumlahKandang.toLocaleString('id-ID'), - }, - ]; - }, []); - return ( <>
- {filteredData.length === 0 && ( + {isLoadingDepreciations && ( +
+ +
+ )} + + {!isLoadingDepreciations && depreciations.length === 0 && ( { /> )} - {filteredData.length > 0 && ( + {!isLoadingDepreciations && depreciations.length > 0 && ( <> -
- -
- - setPage((currentPage) => - currentPage > 1 ? currentPage - 1 : currentPage - ) - } - onNextPage={() => - setPage((currentPage) => - currentPage < totalPages ? currentPage + 1 : currentPage - ) - } - onPageChange={setPage} - rowOptions={[10, 20, 50, 100]} - onRowChange={(value) => { - setPageSize(value); - setPage(1); + {depreciations.map((depreciationItem, idx) => ( + -
+ variant='bordered' + collapsible={true} + > +
+ + ))} + + setPage(tableFilterState.page - 1)} + onNextPage={() => setPage(tableFilterState.page + 1)} + onPageChange={setPage} + rowOptions={[10, 20, 50, 100]} + onRowChange={setPageSize} + /> )} - { + updateFilter('area_id', values.area_id ?? ''); + updateFilter('location_id', values.location_id ?? ''); + updateFilter('project_flock_id', values.project_flock_id ?? ''); + updateFilter( + 'period', + values.period ? formatDate(values.period, 'YYYY-MM-DD') : '' + ); + + console.log({ values }); }} - > -
-
- -

Filter Data

-
- -
- -
-
- - setDraftFilters((current) => ({ - ...current, - farm: (val as OptionType | null)?.value?.toString() || null, - })) - } - isClearable - className={{ wrapper: 'w-full' }} - /> - - - setDraftFilters((current) => ({ - ...current, - period: (val as OptionType | null)?.value?.toString() || null, - })) - } - isClearable - className={{ wrapper: 'w-full' }} - /> -
- -
- - -
- -
+ /> ); }; -const filterToOption = ( - options: OptionType[], - value: string | null -): OptionType | null => { - if (!value) return null; - return options.find((option) => option.value === value) || null; -}; - export default ReportDepreciationTab; From 5a668c469f84872f7c49d0430b08e2e9686994d7 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 20 Apr 2026 00:29:34 +0700 Subject: [PATCH 15/32] chore: update ReportExpenseApi import path --- src/components/pages/report/expense/tab/ReportExpenseTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx index e439c09a..811d9c2c 100644 --- a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -23,7 +23,7 @@ import RealizationStatusBadge from '@/components/pages/expense/RealizationStatus import Table from '@/components/Table'; import { formatCurrency, formatDate } from '@/lib/helper'; import { ReportExpense } from '@/types/api/report/report-expense'; -import { ReportExpenseApi } from '@/services/api/report'; +import { ReportExpenseApi } from '@/services/api/report/expense-report'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import Modal, { useModal } from '@/components/Modal'; From aa4da686c63473c32f999321028626e5c5c2277c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 20 Apr 2026 00:29:55 +0700 Subject: [PATCH 16/32] fix: move and rename report to expense-report.ts --- .../api/{report.ts => report/expense-report.ts} | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) rename src/services/api/{report.ts => report/expense-report.ts} (72%) diff --git a/src/services/api/report.ts b/src/services/api/report/expense-report.ts similarity index 72% rename from src/services/api/report.ts rename to src/services/api/report/expense-report.ts index 5a5e06c8..e6faf8ac 100644 --- a/src/services/api/report.ts +++ b/src/services/api/report/expense-report.ts @@ -1,7 +1,10 @@ import { BaseApiService } from '@/services/api/base'; import { httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; -import { ReportExpense } from '@/types/api/report/report-expense'; +import { + ReportDepreciation, + ReportExpense, +} from '@/types/api/report/report-expense'; export class ReportExpenseApiService extends BaseApiService< ReportExpense, @@ -20,3 +23,9 @@ export class ReportExpenseApiService extends BaseApiService< } export const ReportExpenseApi = new ReportExpenseApiService('/reports/expense'); + +export const DepreciationReportApi = new BaseApiService< + ReportDepreciation, + unknown, + unknown +>('/reports/expense/depreciation'); From f49822d03de7f5c31406cba1269b0b4c1341c7b6 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 20 Apr 2026 00:30:07 +0700 Subject: [PATCH 17/32] fix: adjust ReportDepreciation type --- src/types/api/report/report-expense.d.ts | 37 +++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/types/api/report/report-expense.d.ts b/src/types/api/report/report-expense.d.ts index f513b249..cf0d3ad2 100644 --- a/src/types/api/report/report-expense.d.ts +++ b/src/types/api/report/report-expense.d.ts @@ -54,13 +54,36 @@ export type ReportExpenseSearchParams = { }; export type ReportDepreciation = { - id: string; - flock: string; - totalCostPullet: number; - totalDepresiasi: number; - periode: string; - farm: string; - jumlahKandang: number; + project_flock_id: number; + farm_name: string; + period: string; + depreciation_percent_effective: number; + depreciation_value: number; + pullet_cost_day_n_total: number; + hpp?: number; + components: { + kandang: { + kandang_id: number; + hpp?: number; + transfer_id: number; + cutover_date: string; + kandang_name: string; + manual_input_id: number; + depreciation_value: number; + start_schedule_day: number; + depreciation_percent: number; + source_project_flock_id: number; + day_n: number; + transfer_date: string; + pullet_cost_day_n: number; + depreciation_source: string; + project_flock_kandang_id: number; + transfer_qty: number; + house_type: string; + origin_date: string; + }[]; + kandang_count: number; + }; }; export type ReportDepreciationSearchParams = { From 267a6f37cc12b70e33afca3aa74f1a59f07ae85c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 20 Apr 2026 01:13:32 +0700 Subject: [PATCH 18/32] chore: remove unnecessary code --- .../pages/report/expense/tab/ReportDepreciationTab.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx index 8c6caaa3..41c2f9e8 100644 --- a/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx +++ b/src/components/pages/report/expense/tab/ReportDepreciationTab.tsx @@ -246,8 +246,6 @@ const ReportDepreciationTab = ({ tabId }: ReportDepreciationTabProps) => { 'period', values.period ? formatDate(values.period, 'YYYY-MM-DD') : '' ); - - console.log({ values }); }} /> From 5b5113de6ede5ac52f9bbbc43ee7c618d818f5c0 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 20 Apr 2026 01:13:51 +0700 Subject: [PATCH 19/32] fix: add page and pageSize --- .../pages/report/marketing/filter/DailyMarketingFilter.ts | 4 ++++ .../pages/report/marketing/filter/HppPerKandangFilter.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/components/pages/report/marketing/filter/DailyMarketingFilter.ts b/src/components/pages/report/marketing/filter/DailyMarketingFilter.ts index 85c765a9..600a7f7d 100644 --- a/src/components/pages/report/marketing/filter/DailyMarketingFilter.ts +++ b/src/components/pages/report/marketing/filter/DailyMarketingFilter.ts @@ -1,6 +1,8 @@ import * as yup from 'yup'; export type DailyMarketingReportFilterType = { + page?: number; + pageSize?: number; search: string | null; area_id: string | null; location_id: string | null; @@ -14,6 +16,8 @@ export type DailyMarketingReportFilterType = { }; export const DailyMarketingReportFilterSchema = yup.object({ + page: yup.number().nullable(), + pageSize: yup.number().nullable(), search: yup.string().nullable(), area_id: yup.string().nullable(), location_id: yup.string().nullable(), diff --git a/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts b/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts index 57d2dcd2..209f99a7 100644 --- a/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts +++ b/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts @@ -1,6 +1,8 @@ import * as yup from 'yup'; export type HppPerKandangFilterType = { + page?: number; + pageSize?: number; area_id: string | null; location_id: string | null; kandang_id: string | null; @@ -12,6 +14,8 @@ export type HppPerKandangFilterType = { }; export const HppPerKandangFilterSchema = yup.object({ + page: yup.number().nullable(), + pageSize: yup.number().nullable(), area_id: yup.string().nullable(), location_id: yup.string().nullable(), kandang_id: yup.string().nullable(), From 898bbd57ec8edf63c728513a05d1f1bcadbde467 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 20 Apr 2026 01:14:10 +0700 Subject: [PATCH 20/32] fix: pass page and pageSize to Table component --- .../marketing/tab/DailyMarketingTab.tsx | 31 ++++++++++++++++ .../report/marketing/tab/HppPerKandangTab.tsx | 36 +++++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 7a984472..01e1eea9 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -53,6 +53,8 @@ interface DailyMarketingTabProps { } interface FilterParams { + page?: number; + pageSize?: number; area_id?: string; location_id?: string; warehouse_id?: string; @@ -116,6 +118,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { // ===== FORMIK SETUP ===== const formik = useFormik({ initialValues: { + page: 1, + pageSize: 10, search: null, area_id: null, location_id: null, @@ -130,6 +134,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { validationSchema: DailyMarketingReportFilterSchema, onSubmit: (values, { setSubmitting }) => { setFilterParams({ + page: values.page || undefined, + pageSize: values.pageSize || undefined, area_id: values.area_id || undefined, location_id: values.location_id || undefined, warehouse_id: values.warehouse_id || undefined, @@ -222,6 +228,9 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { const params = new URLSearchParams(); if (searchValue) params.set('search', searchValue); + if (filterParams.page) params.set('page', String(filterParams.page)); + if (filterParams.pageSize) + params.set('limit', String(filterParams.pageSize)); if (filterParams.area_id) params.set('area_id', filterParams.area_id); if (filterParams.location_id) params.set('location_id', filterParams.location_id); @@ -283,6 +292,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { if (filterParams.marketing_type) params.set('marketing_type', filterParams.marketing_type); if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by); + params.set('page', '1'); params.set('limit', '9999999'); const queryString = `?${params.toString()}`; @@ -688,6 +698,27 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
+ setFilterParams((prevVal) => ({ ...prevVal, page: newPage })) + } + onPageSizeChange={(newPageSize) => + setFilterParams((prevVal) => ({ + ...prevVal, + pageSize: newPageSize, + })) + } + isLoading={isLoading} renderFooter={data.length > 0} className={{ containerClassName: 'w-full mb-0!', diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 08f4e890..c290618a 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -40,6 +40,8 @@ interface HppPerKandangTabProps { } interface FilterParams { + page?: number; + pageSize?: number; area_id?: string; location_id?: string; kandang_id?: string; @@ -108,6 +110,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { // ===== FORMIK SETUP ===== const formik = useFormik({ initialValues: { + page: 1, + pageSize: 10, area_id: null, location_id: null, kandang_id: null, @@ -120,6 +124,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { validationSchema: HppPerKandangFilterSchema, onSubmit: (values, { setSubmitting }) => { setFilterParams({ + page: values.page || undefined, + pageSize: values.pageSize || undefined, area_id: values.area_id || undefined, location_id: values.location_id || undefined, kandang_id: values.kandang_id || undefined, @@ -257,6 +263,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { period: filterParams.period, sort_by: filterParams.sort_by, show_unrecorded: filterParams.show_unrecorded, + page: filterParams.page, + pageSize: filterParams.pageSize, }; return ['hpp-per-kandang-report', params]; @@ -271,7 +279,9 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { params.weight_max, params.period, params.sort_by, - params.show_unrecorded + params.show_unrecorded, + params.page, + params.pageSize ) ); @@ -321,7 +331,9 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { params.weight_max, params.period, params.sort_by, - params.show_unrecorded + params.show_unrecorded, + params.page, + params.limit ); return isResponseSuccess(response) ? response.data : null; @@ -466,6 +478,7 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
handleFilterModalOpenRef.current()} variant='outline' className='px-3 py-2.5' @@ -845,6 +858,25 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => {
+ setFilterParams((prevVal) => ({ ...prevVal, page: newPage })) + } + onPageSizeChange={(newPageSize) => + setFilterParams((prevVal) => ({ + ...prevVal, + pageSize: newPageSize, + })) + } + isLoading={isLoading} renderFooter={data.length > 0} renderCustomRow={renderCustomRow} className={{ From 5bf3d3263685c9cfcb7341df5a59fda096f0d471 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 20 Apr 2026 16:21:17 +0700 Subject: [PATCH 21/32] feat: implement bulk approve & reject --- .../ListDailyChecklistContent.tsx | 308 +++++++++++++++++- 1 file changed, 300 insertions(+), 8 deletions(-) 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 01e567d3..7c4befee 100644 --- a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx @@ -40,11 +40,12 @@ import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import Table from '@/components/Table'; import { DailyChecklist } from '@/types/api/daily-checklist/daily-checklist'; import { cn } from '@/lib/helper'; -import { ColumnDef } from '@tanstack/react-table'; +import { ColumnDef, Row } from '@tanstack/react-table'; import { useSelect } from '@/components/input/SelectInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import RequirePermission from '@/components/helper/RequirePermission'; import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang'; +import CheckboxInput from '@/components/input/CheckboxInput'; const STATUS_OPTIONS = [ { value: 'ALL', label: 'Semua Status' }, @@ -122,12 +123,29 @@ export function ListDailyChecklistContent() { // Modals const [showApproveModal, setShowApproveModal] = useState(false); + const [showBulkApproveModal, setShowBulkApproveModal] = useState(false); const [showRejectModal, setShowRejectModal] = useState(false); + const [showBulkRejectModal, setShowBulkRejectModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [selectedItem, setSelectedItem] = useState(null); const [rejectReason, setRejectReason] = useState(''); const [actionLoading, setActionLoading] = useState(false); + const [rowSelection, setRowSelection] = useState>({}); + const selectedRowIds = Object.keys(rowSelection); + + const selectedRowItems = selectedRowIds.map((itemId) => + checklistList.find((item) => item.id === parseInt(itemId)) + ); + + const tableEnableRowSelectionHandler: ( + row: Row + ) => boolean = (row) => { + return ( + row.original.status !== 'APPROVED' && row.original.status !== 'REJECTED' + ); + }; + const handleDetail = (item: DailyChecklist) => { router.push( `/daily-checklist/list-daily-checklist/detail?checklistId=${item.id}` @@ -149,12 +167,21 @@ export function ListDailyChecklistContent() { setShowApproveModal(true); }; + const handleBulkApprove = () => { + setShowBulkApproveModal(true); + }; + const handleReject = (item: DailyChecklist) => { setSelectedItem(item); setRejectReason(''); setShowRejectModal(true); }; + const handleBulkReject = () => { + setRejectReason(''); + setShowBulkRejectModal(true); + }; + const handleDelete = (item: DailyChecklist) => { // ✅ VALIDATION: Only DRAFT can be deleted if (item.status !== 'DRAFT') { @@ -195,6 +222,31 @@ export function ListDailyChecklistContent() { } }; + const confirmBulkApprove = async () => { + if (!selectedRowIds.length) return; + + try { + setActionLoading(true); + + const approveRes = await DailyChecklistApi.bulkApprove(selectedRowIds); + + if (isResponseError(approveRes)) { + toast.error('Gagal approve checklist: ' + approveRes.message); + return; + } + + refreshChecklistList(); + toast.success('Checklist berhasil di-approve'); + setShowBulkApproveModal(false); + setRowSelection({}); + } catch (error) { + console.error('Error approving checklist:', error); + toast.error('Terjadi kesalahan'); + } finally { + setActionLoading(false); + } + }; + const confirmReject = async () => { if (!selectedItem) return; @@ -229,6 +281,40 @@ export function ListDailyChecklistContent() { } }; + const confirmBulkReject = async () => { + if (!selectedRowIds.length) return; + + if (!rejectReason.trim()) { + toast.error('Alasan reject harus diisi'); + return; + } + + try { + setActionLoading(true); + + const rejectRes = await DailyChecklistApi.bulkReject( + selectedRowIds, + rejectReason + ); + + if (isResponseError(rejectRes)) { + toast.error('Gagal reject checklist: ' + rejectRes.message); + return; + } + + refreshChecklistList(); + toast.success('Checklist berhasil di-reject'); + setShowBulkRejectModal(false); + setRowSelection({}); + setRejectReason(''); + } catch (error) { + console.error('Error rejecting checklist:', error); + toast.error('Terjadi kesalahan'); + } finally { + setActionLoading(false); + } + }; + const confirmDelete = async () => { if (!selectedItem) return; @@ -325,6 +411,37 @@ export function ListDailyChecklistContent() { }; const checklistListColumns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( +
+ +
+ ), + cell: ({ row }) => { + const isCheckboxDisabled = + !row.getCanSelect() || + row.original.status === 'APPROVED' || + row.original.status === 'REJECTED'; + + return ( +
+ +
+ ); + }, + }, { accessorKey: 'date', header: 'Tanggal', @@ -459,13 +576,39 @@ export function ListDailyChecklistContent() {
{/* Page Title */} -
-

- List Daily Checklist -

-

- Daftar semua checklist harian -

+
+
+

+ List Daily Checklist +

+

+ Daftar semua checklist harian +

+
+ + + {selectedRowIds.length > 0 && ( +
+ + +
+ )} +
{/* Main Card */} @@ -588,6 +731,10 @@ export function ListDailyChecklistContent() { } onPageChange={setPage} isLoading={isLoadingChecklistList} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + enableRowSelection={tableEnableRowSelectionHandler} + withCheckbox className={{ containerClassName: cn({ 'w-full mb-20': @@ -666,6 +813,76 @@ export function ListDailyChecklistContent() { + {/* Bulk Approve Modal */} + + + + Approve Checklist + + Apakah Anda yakin ingin approve {selectedRowIds.length} checklist + ini? + + + +
+ {selectedRowItems.map((item) => ( +
+
+ Tanggal: + + {formatDate(item?.date ?? '')} + +
+
+ Kandang: + + {item?.kandang?.name ?? '-'} + +
+
+ Kategori: + + {item?.category + ? (CATEGORY_LABELS[item.category] ?? item?.category) + : item?.category} + +
+
+ Progress: + + {item?.progress}% + +
+
+ ))} +
+ + + + + +
+
+ {/* Reject Modal */} @@ -735,6 +952,81 @@ export function ListDailyChecklistContent() { + {/* Bulk Reject Modal */} + + + + Reject Checklist + + Berikan alasan reject untuk checklist ini + + + +
+ {selectedRowItems.map((item) => ( +
+
+ Tanggal: + + {formatDate(item?.date ?? '')} + +
+
+ Kandang: + + {item?.kandang?.name ?? '-'} + +
+
+ Kategori: + + {item?.category + ? CATEGORY_LABELS[item.category] || item?.category + : item?.category} + +
+
+ ))} +
+ +
+ +