From 9dc8f0553449ee5b789acebd28308f4bd5553668 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 10:55:38 +0700 Subject: [PATCH 001/149] feat: adjust penjualan calculation and delivery order logic --- .../pages/marketing/SalesOrderFormModal.tsx | 2 +- .../marketing/form/MarketingForm.schema.ts | 7 +---- .../DeliverOrderProduct.schema.ts | 28 +++++-------------- .../delivery-order/DeliverOrderProduct.tsx | 21 +++++++------- .../sales-order/SalesOrderProduct.schema.ts | 28 +++++-------------- .../sales-order/SalesOrderProductForm.tsx | 21 +++++++------- .../table-view/SalesOrderProductTable.tsx | 2 +- src/lib/marketing-calculation.ts | 4 +-- 8 files changed, 41 insertions(+), 72 deletions(-) diff --git a/src/components/pages/marketing/SalesOrderFormModal.tsx b/src/components/pages/marketing/SalesOrderFormModal.tsx index 66acc440..4f5d4b95 100644 --- a/src/components/pages/marketing/SalesOrderFormModal.tsx +++ b/src/components/pages/marketing/SalesOrderFormModal.tsx @@ -208,7 +208,7 @@ const SalesOrderFormModal = ({ convertion_unit: normalizedConvertionUnit, weight_per_convertion: product.weight_per_convertion ?? undefined, - week: product.week?.value ?? undefined, + week: product.week ?? undefined, } as CreateSalesOrderProductPayload; }), } as CreateSalesOrderPayload) diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index 0215217f..17b6d78c 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -128,12 +128,7 @@ export const SalesProductToFieldValues = ( label: formatTitleCase(product.convertion_unit), } : null, - week: product.week - ? { - value: product.week, - label: `Week ${product.week}`, - } - : null, + week: product.week ?? null, total_peti: product.total_peti, weight_per_convertion: product.weight_per_convertion, uom: product.product_warehouse.product.uom.name, diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts index 4c20f05b..c7cb4e9f 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema.ts @@ -30,13 +30,7 @@ type DeliveryOrderProductSchemaType = { /** Harga per butir telur untuk TELUR + QTY */ price_per_qty?: number | null | undefined; /** Week untuk ayam pullet */ - week?: - | { - value?: number; - label?: string; - } - | null - | undefined; + week?: number | null | undefined; }; export const DeliveryOrderProductSchema: Yup.ObjectSchema = @@ -79,26 +73,18 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema marketingType?.value?.toLowerCase() === 'ayam_pullet', then: (schema) => schema - .shape({ - value: Yup.number().required( - 'Week wajib diisi untuk Ayam Pullet!' - ), - label: Yup.string().required( - 'Week wajib diisi untuk Ayam Pullet!' - ), - }) - .required('Week wajib diisi untuk Ayam Pullet!'), + .min(1, 'Week wajib diisi untuk Ayam Pullet!') + .required('Week wajib diisi untuk Ayam Pullet!') + .typeError('Week harus berupa angka!'), otherwise: (schema) => schema.optional().notRequired(), }), }); diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index 850d88d2..d7b97d8e 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -511,19 +511,20 @@ const DeliveryOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', val); + name='week' + value={formik.values.week ?? undefined} + onChange={(e) => { + formik.setFieldValue('week', Number(e.target.value)); + setCurrentInput(e.target.name); }} - placeholder='Pilih Week' + onBlur={() => handleBlurField('week')} + isError={formik.touched.week && Boolean(formik.errors.week)} + errorMessage={formik.errors.week as string} + placeholder='Masukan Minggu' + decimalScale={0} /> )} diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts index f17f6f8c..390756e7 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts @@ -37,13 +37,7 @@ type SalesOrderProductSchemaType = { /** Harga per butir telur untuk TELUR + QTY */ price_per_qty?: number | null | undefined; /** Week untuk ayam pullet */ - week?: - | { - value?: number; - label?: string; - } - | null - | undefined; + week?: number | null | undefined; }; export const SalesOrderProductSchema: Yup.ObjectSchema = @@ -102,26 +96,18 @@ export const SalesOrderProductSchema: Yup.ObjectSchema marketingType?.value?.toLowerCase() === 'ayam_pullet', then: (schema) => schema - .shape({ - value: Yup.number().required( - 'Week wajib diisi untuk Ayam Pullet!' - ), - label: Yup.string().required( - 'Week wajib diisi untuk Ayam Pullet!' - ), - }) - .required('Week wajib diisi untuk Ayam Pullet!'), + .min(1, 'Week wajib diisi untuk Ayam Pullet!') + .required('Week wajib diisi untuk Ayam Pullet!') + .typeError('Week harus berupa angka!'), otherwise: (schema) => schema.optional().notRequired(), }), }); diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index c718c40c..ac67cff4 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -467,19 +467,20 @@ const SalesOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', val); + name='week' + value={formik.values.week ?? undefined} + onChange={(e) => { + formik.setFieldValue('week', Number(e.target.value)); + setCurrentInput(e.target.name); }} - placeholder='Pilih Week' + onBlur={() => handleBlurField('week')} + isError={formik.touched.week && Boolean(formik.errors.week)} + errorMessage={formik.errors.week as string} + placeholder='Masukan Minggu' + decimalScale={0} /> )} diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 6f667f76..e900974b 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -234,7 +234,7 @@ const SalesOrderProductTable = ({ 'ayam_pullet' && ( Tipe Konversi - {item.week?.label} + Week {item.week} )} {item.convertion_unit?.value.toLowerCase() === 'peti' && ( diff --git a/src/lib/marketing-calculation.ts b/src/lib/marketing-calculation.ts index 5715ec88..5ad5a1e6 100644 --- a/src/lib/marketing-calculation.ts +++ b/src/lib/marketing-calculation.ts @@ -15,7 +15,7 @@ export type MarketingFormValues = { total_price?: string | number; marketing_type?: { value: string; label: string } | null; convertion_unit?: { value: string; label: string } | null; - week?: { value?: number; label?: string } | null; + week?: number | null; weight_per_convertion?: number | null; price_per_convertion?: number | null; total_peti?: number | null; @@ -100,7 +100,7 @@ export const calculateAyamPullet = ( ): void => { const { values, setFieldValue } = ctx; const unitPrice = Number(values.unit_price || 0); - const week = Number(values.week?.value || 0); + const week = Number(values.week || 0); const qty = Number(values.qty || 0); const avgWeight = Number(values.avg_weight || 0); const totalWeight = Number(values.total_weight || 0); From 42088e51a842beb408fdcc0c223c9e273a6fcdec Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 13:13:57 +0700 Subject: [PATCH 002/149] refactor(FE): Refactor marketing filter to use unique customer options --- .../pages/marketing/MarketingFilter.tsx | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/components/pages/marketing/MarketingFilter.tsx b/src/components/pages/marketing/MarketingFilter.tsx index 3c59e07e..eda3d9b2 100644 --- a/src/components/pages/marketing/MarketingFilter.tsx +++ b/src/components/pages/marketing/MarketingFilter.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject } from 'react'; +import { RefObject, useMemo } from 'react'; import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import Modal from '@/components/Modal'; @@ -9,10 +9,12 @@ import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; -import { CustomerApi, ProductApi } from '@/services/api/master-data'; import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; import { MarketingFilter } from '@/types/api/marketing/marketing'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import { MarketingApi } from '@/services/api/marketing/marketing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { BaseMarketing, BaseSalesOrder } from '@/types/api/marketing/marketing'; interface MarketingFilterModal { ref: RefObject; @@ -31,21 +33,52 @@ const MarketingFilterModal = ({ // ===== OPTIONS ===== const { - options: productsOptions, + rawData: productsRawData, isLoadingOptions: isLoadingProductsOptions, setInputValue: setProductsInputValue, loadMore: loadMoreProducts, - } = useSelect(ProductApi.basePath, 'id', 'name', '', { + } = useSelect(MarketingApi.basePath, 'id', 'so_number', '', { limit: 'limit', }); + + const productsOptions = useMemo(() => { + if (!productsRawData || !isResponseSuccess(productsRawData)) return []; + + const productsMap = new Map(); + + productsRawData.data.forEach((deliveryOrder: BaseMarketing) => { + deliveryOrder.sales_order?.forEach((so: BaseSalesOrder) => { + const product = so.product_warehouse?.product; + if (product?.id && product?.name) { + productsMap.set(product.id, { + value: product.id, + label: product.name, + }); + } + }); + }); + + return Array.from(productsMap.values()); + }, [productsRawData]); + const { options: customersOptions, isLoadingOptions: isLoadingCustomersOptions, setInputValue: setCustomersInputValue, loadMore: loadMoreCustomers, - } = useSelect(CustomerApi.basePath, 'id', 'name', '', { + } = useSelect(MarketingApi.basePath, 'customer.id', 'customer.name', '', { limit: 'limit', }); + + const uniqueCustomersOptions = useMemo(() => { + const seen = new Set(); + return customersOptions.filter((customer) => { + if (seen.has(customer.value)) return false; + seen.add(customer.value); + return true; + }); + }, [customersOptions]); + const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({ value: item.step_name.split(' ').join('_').toUpperCase(), label: item.step_name, @@ -151,7 +184,7 @@ const MarketingFilterModal = ({ label='Customer' isClearable placeholder='Pilih customer' - options={customersOptions} + options={uniqueCustomersOptions} isLoading={isLoadingCustomersOptions} value={formik.values.customer_id} onChange={customerChangeHandler} From 5e9ce703209c97c342323fd1555ff805237fca46 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 13:21:33 +0700 Subject: [PATCH 003/149] feat(FE): Add "Ditolak" option to statusOptions in MarketingFilter --- src/components/pages/marketing/MarketingFilter.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/pages/marketing/MarketingFilter.tsx b/src/components/pages/marketing/MarketingFilter.tsx index eda3d9b2..3f56854e 100644 --- a/src/components/pages/marketing/MarketingFilter.tsx +++ b/src/components/pages/marketing/MarketingFilter.tsx @@ -79,10 +79,13 @@ const MarketingFilterModal = ({ }); }, [customersOptions]); - const statusOptions = MARKETING_APPROVAL_LINE.map((item) => ({ - value: item.step_name.split(' ').join('_').toUpperCase(), - label: item.step_name, - })); + const statusOptions = [ + ...MARKETING_APPROVAL_LINE.map((item) => ({ + value: item.step_name.split(' ').join('_').toUpperCase(), + label: item.step_name, + })), + { value: 'DITOLAK', label: 'Ditolak' }, + ]; const formik = useFormik<{ product_ids: OptionType[]; From 304be4f4325e25542c0ee2b94f163488e4a2afa0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 13:29:38 +0700 Subject: [PATCH 004/149] feat(FE): Add support for displaying DO number in MarketingTable --- src/components/pages/marketing/MarketingTable.tsx | 9 +++++++-- src/types/api/marketing/marketing.d.ts | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 0a35a8bc..5f88c8b3 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -379,8 +379,13 @@ const MarketingTable = () => { }, }, { - accessorKey: 'so_number', + accessorKey: 'so_do_number', header: 'No. Order', + cell: (props) => { + return props.row.original.do_number + ? props.row.original.do_number + : props.row.original.so_number; + }, }, { accessorKey: 'so_date', @@ -408,7 +413,7 @@ const MarketingTable = () => { : approval?.step_number == 2 ? 'info' : approval?.step_number == 3 - ? 'warning' + ? 'success' : 'neutral' : 'neutral' } diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index 80a0b90b..12b0ee2c 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -17,6 +17,8 @@ export type BaseMarketing = { status?: string; so_number: string; so_date: string; + do_number?: string; + do_date?: string; customer: Customer; sales_person: CreatedUser; notes: string; From 08d1447d11672715665101a7513265db8380981c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 13:41:17 +0700 Subject: [PATCH 005/149] refactor(FE): Update button label based on approval step number --- src/components/pages/marketing/MarketingTable.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 5f88c8b3..6368df11 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -109,7 +109,9 @@ const RowsOptionsMenu = ({ className='p-3 justify-start text-sm font-semibold w-full' > - Deliver Item + {props.row.original.latest_approval.step_number == 2 + ? 'Deliver Item' + : 'Edit Delivery'} From c9a5a91970c80404cab815013965aee18ae3bc5b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 14:20:10 +0700 Subject: [PATCH 006/149] refactor(FE): Update order number display logic in DeliveryOrderFormModal --- src/components/pages/marketing/DeliveryOrderFormModal.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 7c953fe8..75f1bf1e 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -562,9 +562,11 @@ const DeliveryOrderFormModal = ({ - No. Sales Order + No. Order - {marketing.data.so_number} + {marketing.data.do_number + ? marketing.data.do_number + : marketing.data.so_number} From 4d23929924bd22beb6b64667073694f2f4f8f813 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 14:20:52 +0700 Subject: [PATCH 007/149] refactor(FE): Update approval logic and conditional rendering in delivery forms --- .../marketing/DeliveryOrderFormModal.tsx | 3 ++- .../table-view/DeliveryOrderProductTable.tsx | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 75f1bf1e..a2feceac 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -430,7 +430,8 @@ const DeliveryOrderFormModal = ({ const isPending = useMemo(() => { return ( isResponseSuccess(marketing) && - marketing.data.latest_approval.step_number === 1 + marketing.data.latest_approval.step_number === 1 && + marketing.data.latest_approval.action === 'PENDING' ); }, [marketing]); diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index 885cd8fc..93d4f658 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -39,6 +39,8 @@ const DeliveryOrderProductTable = ({ const onDeleteRef = useRef(onDelete); onDeleteRef.current = onDelete; + const approvalStepNumber = marketing?.latest_approval?.step_number; + return ( <>
@@ -105,16 +107,20 @@ const DeliveryOrderProductTable = ({ <> - - Tanggal Pengiriman - - {item.delivery_date ? ( - formatDate(item.delivery_date, 'DD MMM YYYY') - ) : ( - Belum diisi - )} - - + {approvalStepNumber !== 1 && ( + + + Tanggal Pengiriman + + + {item.delivery_date ? ( + formatDate(item.delivery_date, 'DD MMM YYYY') + ) : ( + Belum diisi + )} + + + )} {item.do_number && ( No. Pengiriman From 3bacc59dc610167e2601c56ce7faca1062822261 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 14:32:30 +0700 Subject: [PATCH 008/149] refactor(FE): Add condition to hide buttons when step number is 3 --- .../marketing/DeliveryOrderFormModal.tsx | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index a2feceac..9027f63e 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -718,31 +718,33 @@ const DeliveryOrderFormModal = ({ />
)} - {step === 1 && ( -
- - -
- )} + {step === 1 && + marketing?.data?.latest_approval?.step_number !== 3 && ( +
+ + +
+ )} + {step === 3 && null} )} From 9ee5e95d0b0fd07a4e197df319bfd182a0deaa54 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 14:33:13 +0700 Subject: [PATCH 009/149] refactor(FE): Remove unused conditional rendering for step 3 --- src/components/pages/marketing/DeliveryOrderFormModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 9027f63e..a1885709 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -744,7 +744,6 @@ const DeliveryOrderFormModal = ({ )} - {step === 3 && null} )} From 4e80c1a7037aac71d93c415764f1707f485fc683 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 14:46:13 +0700 Subject: [PATCH 010/149] refactor(FE): Fix warehouse name display in DeliveryOrderProductTable --- .../marketing/form/table-view/DeliveryOrderProductTable.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index 93d4f658..c77faba1 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -136,7 +136,9 @@ const DeliveryOrderProductTable = ({ Gudang - {item.marketing_product?.product_warehouse?.label} + {doItem?.warehouse?.name || + item.marketing_product?.product_warehouse_data + ?.warehouse?.name} From 54a6e7e2471fb64b50ca939df912d087649d12d3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 7 Feb 2026 09:00:04 +0700 Subject: [PATCH 011/149] refactor(FE): Refactor RecordingForm to simplify egg product filtering --- .../recording/form/RecordingForm.tsx | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index b8b6b1fc..e056663f 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -606,7 +606,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { isLoadingOptions: isLoadingEggProducts, loadMore: loadMoreEggProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { - search: 'telur', + type: 'TELUR', location_id: eggProductsLocationId, kandang_id: eggProductsKandangId, }); @@ -886,20 +886,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (isResponseSuccess(eggProductsData) && selectedKandang) { const data = eggProductsData.data as unknown as ProductWarehouse[]; data.forEach((product) => { - const productName = product.product.name; - - if ( - productName.toLowerCase().includes('telur') || - productName.toLowerCase().includes('egg') || - productName.toLowerCase().includes('pecah') || - productName.toLowerCase().includes('konsumsi') || - productName.toLowerCase().includes('baik') - ) { - options.push({ - value: product.id, - label: product.product.name, - }); - } + options.push({ + value: product.id, + label: product.product.name, + }); }); } From 5ac958231abcc13887f23a9dd75faa8aed37b598 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 7 Feb 2026 09:10:39 +0700 Subject: [PATCH 012/149] refactor(FE): Remove unused invoice download functionality from PurchaseTable --- .../pages/purchase/PurchaseTable.tsx | 64 +------------------ .../purchase/order/PurchaseOrderInvoice.tsx | 30 ++------- 2 files changed, 5 insertions(+), 89 deletions(-) diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 2c08f726..b9ff2a56 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -17,7 +17,6 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; -import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice'; import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -159,27 +158,6 @@ const PurchaseTable = () => { PurchaseApi.getAllFetcher ); - const [isDownloadingInvoice, setIsDownloadingInvoice] = useState(false); - const [invoicePurchaseData, setInvoicePurchaseData] = - useState(null); - - const handleDownloadInvoice = async (purchaseId: number) => { - setIsDownloadingInvoice(true); - try { - const response = await PurchaseApi.getSingle(purchaseId); - if (isResponseSuccess(response) && response.data) { - setInvoicePurchaseData(response.data); - setTimeout(() => { - setInvoicePurchaseData(null); - }, 1000); - } - } catch { - toast.error('Gagal mengambil data purchase order.'); - } finally { - setIsDownloadingInvoice(false); - } - }; - // ===== TABLE COLUMNS DEFINITION ===== const purchaseColumns: ColumnDef[] = [ { @@ -190,38 +168,7 @@ const PurchaseTable = () => { }, }, { - accessorKey: 'po_expedition', - header: 'PO Ekspedisi', - cell: (props) => { - const purchase = props.row.original; - - if (!purchase.po_number || purchase.po_number === 'Belum dibuat') { - return -; - } - - return ( - - ); - }, - }, - { - accessorKey: 'supplier.name', + accessorKey: 'supplier', header: 'Vendor', cell: (props) => props.row.original.supplier.name, }, @@ -505,15 +452,6 @@ const PurchaseTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> - - {invoicePurchaseData && ( -
- -
- )} ); }; diff --git a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx index 4ad093e1..aed154d0 100644 --- a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useMemo, useState, useEffect, useCallback, useRef } from 'react'; +import { useMemo, useState } from 'react'; import { Page, Text, @@ -235,16 +235,11 @@ const pdfStyles = StyleSheet.create({ interface PurchaseOrderInvoiceProps { data?: Purchase; className?: string; - triggerDownloadOnMount?: boolean; } -const PurchaseOrderInvoice = ({ - data, - triggerDownloadOnMount, -}: PurchaseOrderInvoiceProps) => { +const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { const [, setIsGeneratingPDF] = useState(false); const purchaseData = data; - const hasDownloadedRef = useRef(false); const grandTotal = useMemo(() => { return ( @@ -255,7 +250,7 @@ const PurchaseOrderInvoice = ({ ); }, [purchaseData?.items]); - const handleDownloadPDF = useCallback(async () => { + const handleDownloadPDF = async () => { if (!purchaseData) { toast.error('No purchase order data available'); return; @@ -515,20 +510,7 @@ const PurchaseOrderInvoice = ({ } finally { setIsGeneratingPDF(false); } - }, [purchaseData]); - - useEffect(() => { - if (triggerDownloadOnMount && purchaseData && !hasDownloadedRef.current) { - hasDownloadedRef.current = true; - handleDownloadPDF(); - } - }, [triggerDownloadOnMount, purchaseData]); - - useEffect(() => { - if (!triggerDownloadOnMount) { - hasDownloadedRef.current = false; - } - }, [triggerDownloadOnMount]); + }; if (!purchaseData) { return ( @@ -538,10 +520,6 @@ const PurchaseOrderInvoice = ({ ); } - if (triggerDownloadOnMount) { - return null; - } - return purchaseData?.po_number && purchaseData.po_number !== 'Belum dibuat' ? ( @@ -753,8 +741,8 @@ const DeliveryOrderFormModal = ({ ref={successModal.ref} iconPosition='left' type='success' - text={`${modalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`} - subtitleText={`${modalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`} + text={`${currentModalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`} + subtitleText={`${currentModalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`} primaryButton={{ text: 'Oke', color: 'primary', @@ -764,14 +752,18 @@ const DeliveryOrderFormModal = ({ }, }} > - +
+ +
( + modalAction + ); + const isModalActionForForm = modalAction === 'add' || modalAction === 'edit' || @@ -412,6 +416,7 @@ const SalesOrderFormModal = ({ // ================== EFFECT ================== useEffect(() => { if (modalAction === 'add' || modalAction === 'edit') { + setCurrentModalAction(modalAction); formModal.openModal(); } }, [modalAction]); @@ -724,8 +729,8 @@ const SalesOrderFormModal = ({ ref={successModal.ref} iconPosition='left' type='success' - text={`${modalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`} - subtitleText={`${modalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`} + text={`${currentModalAction === 'add' ? 'Data Berhasil Ditambahkan' : 'Data Berhasil Diubah'}`} + subtitleText={`${currentModalAction === 'add' ? 'Data sales order telah berhasil disimpan.' : 'Data sales order telah berhasil diubah.'}`} primaryButton={{ text: 'Oke', color: 'primary', @@ -735,13 +740,15 @@ const SalesOrderFormModal = ({ }, }} > - +
+ +
Date: Sat, 7 Feb 2026 09:33:58 +0700 Subject: [PATCH 015/149] refactor(FE): Increase max height of order form modals to 50vh --- src/components/pages/marketing/DeliveryOrderFormModal.tsx | 2 +- src/components/pages/marketing/SalesOrderFormModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index a7f5be12..76c46ebf 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -752,7 +752,7 @@ const DeliveryOrderFormModal = ({ }, }} > -
+
-
+
Date: Sat, 7 Feb 2026 09:58:00 +0700 Subject: [PATCH 016/149] refactor(FE): Refactor tables to use Card component --- .../table-view/DeliveryOrderProductTable.tsx | 88 ++++++++++--------- .../table-view/SalesOrderProductTable.tsx | 82 ++++++++--------- 2 files changed, 88 insertions(+), 82 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index c77faba1..83ce01d3 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -1,5 +1,6 @@ import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; import Button from '@/components/Button'; +import Card from '@/components/Card'; import { Icon } from '@iconify/react'; import { useRef } from 'react'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; @@ -49,9 +50,50 @@ const DeliveryOrderProductTable = ({ (doItem) => doItem.do_number === item.do_number ); return ( -
+ + +
+ ) : undefined + } > <> @@ -199,7 +203,7 @@ const DeliveryOrderProductTable = ({
-
-
Value
- {(formType === 'add_delivery' || - formType === 'edit_delivery' || - formType === 'detail') && ( -
- - -
- )} -
+ Value
-
+ ); })}
diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index e900974b..5cf8f9de 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -1,6 +1,7 @@ 'use client'; import Button from '@/components/Button'; +import Card from '@/components/Card'; import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; import { formatCurrency, @@ -146,9 +147,46 @@ const SalesOrderProductTable = ({ <>
{data.map((item) => ( -
+ + +
+ ) : undefined + } > <> @@ -294,7 +296,7 @@ const SalesOrderProductTable = ({
-
-
Value
- {formType !== 'success' && ( -
- - -
- )} -
+ Value
-
+ ))} {formType != 'add_deliver' && formType != 'edit_deliver' && From b85e47f60171d082b5160671219b4c0c0b7e936d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 7 Feb 2026 10:00:02 +0700 Subject: [PATCH 017/149] refactor(FE): Add top border to table rows in product tables --- .../marketing/form/table-view/DeliveryOrderProductTable.tsx | 2 +- .../pages/marketing/form/table-view/SalesOrderProductTable.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index 83ce01d3..80cd42fe 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -102,7 +102,7 @@ const DeliveryOrderProductTable = ({ className='border-none w-full' > - + Label diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 5cf8f9de..502e6c37 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -195,7 +195,7 @@ const SalesOrderProductTable = ({ className='border-none w-full' > - + Label From a8dce9da46509e2e24aa74b8aaecddffa3c69082 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 7 Feb 2026 10:04:15 +0700 Subject: [PATCH 018/149] refactor(FE): Refactor table actions to be part of the "Value" column --- .../table-view/DeliveryOrderProductTable.tsx | 70 +++++++++++-------- .../table-view/SalesOrderProductTable.tsx | 66 +++++++++-------- 2 files changed, 76 insertions(+), 60 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index 80cd42fe..af53685c 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -64,36 +64,6 @@ const DeliveryOrderProductTable = ({ title: 'px-2 py-1.5 font-normal text-sm', collapsible: 'rounded-lg', }} - actions={ - formType === 'add_delivery' || - formType === 'edit_delivery' || - formType === 'detail' ? ( -
- - -
- ) : undefined - } > <> diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 502e6c37..a13a1117 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -159,34 +159,6 @@ const SalesOrderProductTable = ({ title: 'px-2 py-1.5 font-normal text-sm', collapsible: 'rounded-lg', }} - actions={ - formType !== 'success' ? ( -
- - -
- ) : undefined - } >
- Value +
+
Value
+ {(formType === 'add_delivery' || + formType === 'edit_delivery' || + formType === 'detail') && ( +
+ + +
+ )} +
<> From 60ace68daea62d5f2b573e2e77c9741954f18b2c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 08:59:51 +0700 Subject: [PATCH 019/149] refactor(FE): Remove FCR-related fields and functionality --- .../project-flock/ProjectFlockTable.tsx | 4 - .../form/ProjectFlockForm.schema.ts | 14 -- .../project-flock/form/ProjectFlockForm.tsx | 35 ---- .../recording/form/RecordingForm.tsx | 166 +----------------- src/types/api/production/project-flock.d.ts | 4 - 5 files changed, 1 insertion(+), 222 deletions(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index cad76310..7c4973eb 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -363,10 +363,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { accessorKey: 'location.name', header: 'Lokasi', }, - { - accessorKey: 'fcr.name', - header: 'FCR', - }, { accessorKey: 'category', header: 'Kategori', diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts index dc972b14..eb6f236c 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.schema.ts @@ -16,11 +16,6 @@ type ProjectFlockFormSchemaType = { label: string; } | null; category: string; - fcr: { - value: number | string; - label: string; - } | null; - fcr_id: number; production_standard: { value: number | string; label: string; @@ -96,15 +91,6 @@ export const ProjectFlockFormSchema: Yup.ObjectSchema, - fcr_id: initialValues?.fcr?.id ?? 0, production_standard_id: initialValues?.production_standard?.id ?? 0, location_id: initialValues?.location?.id ?? 0, kandang_ids: initialValues?.kandangs?.map( @@ -574,7 +559,6 @@ const ProjectFlockForm = ({ flock_name: values.flock_name as string, area_id: values.area_id as number, category: values.category as string, - fcr_id: values.fcr_id as number, production_standard_id: values.production_standard_id as number, location_id: values.location_id as number, kandang_ids: values.kandang_ids as number[], @@ -996,25 +980,6 @@ const ProjectFlockForm = ({ isClearable isDisabled={formType != 'add'} /> - { - optionChangeHandler(val, 'fcr'); - }} - onInputChange={setInputValueFcr} - onMenuScrollToBottom={loadMoreFcr} - options={optionsFcr} - isLoading={isLoadingFcrs} - isError={ - formik.touched.fcr_id && Boolean(formik.errors.fcr_id) - } - errorMessage={formik.errors.fcr_id as string} - isClearable - isDisabled={formType != 'add'} - /> [] = [ - { - accessorKey: 'weight', - header: 'Weight', - cell: (props) => formatNumber(props.getValue() as number), - }, - { - accessorKey: 'fcr_number', - header: 'FCR Number', - cell: (props) => formatNumber(props.getValue() as number), - }, - { - accessorKey: 'mortality', - header: 'Mortality', - cell: (props) => formatNumber(props.getValue() as number), - }, -]; - const productionStandardColumns: ColumnDef[] = [ { accessorKey: 'week', @@ -253,36 +234,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const approveModal = useModal(); const rejectModal = useModal(); const deleteModal = useModal(); - const fcrStandardModal = useModal(); const productionStandardModal = useModal(); - const [fcrStandards, setFcrStandards] = useState([]); const [productionStandards, setProductionStandards] = useState(null); - const [isFcrModalOpen, setIsFcrModalOpen] = useState(false); const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] = useState(false); - useEffect(() => { - const checkFcrModalOpen = () => { - const isOpen = fcrStandardModal.ref.current?.open || false; - setIsFcrModalOpen(isOpen); - }; - - checkFcrModalOpen(); - - const observer = new MutationObserver(checkFcrModalOpen); - if (fcrStandardModal.ref.current) { - observer.observe(fcrStandardModal.ref.current, { - attributes: true, - attributeFilter: ['open'], - }); - } - - return () => observer.disconnect(); - }, [fcrStandardModal.ref]); - useEffect(() => { const checkProductionStandardModalOpen = () => { const isOpen = productionStandardModal.ref.current?.open || false; @@ -460,24 +419,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ? projectFlockKandangLookupData.data : undefined; - const fcrId = useMemo(() => { - if (type === 'add') { - return projectFlockKandangLookup?.project_flock?.fcr?.id; - } - return initialValues?.project_flock?.fcr?.id; - }, [type, projectFlockKandangLookup, initialValues]); - - const { data: fcr, isLoading: isLoadingFcrStandards } = useSWR( - isFcrModalOpen && fcrId ? `fcr-detail-${fcrId}` : null, - () => FcrApi.getSingle(fcrId!) - ); - - useEffect(() => { - if (fcr?.status === 'success') { - setFcrStandards((fcr.data as FcrWithStandards).fcr_standards || []); - } - }, [fcr]); - const productionStandardId = useMemo(() => { if (type === 'add') { return projectFlockKandangLookup?.project_flock?.production_standard_id; @@ -1942,24 +1883,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { : '-'}

-
- Standard FCR -
- fcrStandardModal.openModal()} - > - {projectFlockKandangLookup?.project_flock?.fcr?.name || - initialValues?.project_flock?.fcr?.name || - '-'} - -
-
Standard Produksi @@ -2150,22 +2073,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)} -
- Standard FCR -
- fcrStandardModal.openModal()} - > - {initialValues.project_flock?.fcr?.name || '-'} - -
-
Standard Produksi @@ -2217,21 +2124,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
- - - - - + <> + + + + + + + + + + + + + {Number(item.avg_weight ?? 0) > 0 && ( + + + + + )} + {Number(item.total_weight ?? 0) > 0 && ( + + + + + )} + + + + + + + + + + + + + <> {approvalStepNumber !== 1 && ( @@ -144,56 +191,6 @@ const DeliveryOrderProductTable = ({ {item.vehicle_number} - - - - - - - - - - - - - - - - - - - - - - - - - - - - {doItem && ( + + + + <> + + + + + + + + + + + + + + + + + {item.marketing_type?.value.toLowerCase() === 'telur' && ( + + + + + )} + {item.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( + + + + + )} + {item.convertion_unit?.value.toLowerCase() === 'peti' && ( + + + + + )} + {item.marketing_type?.value.toLowerCase() !== 'trading' && ( + <> + + + + + + + + + + )} + + + + + + + + + + + + + + + ); + return ( <>
{data.map((item) => ( - -
- Value +
+
Value
+ {formType !== 'success' && ( +
+ + +
+ )} +
FCR (g) - - {initialValues.fcr_value != null - ? `${formatNumber(initialValues.fcr_value)} g` - : '-'} - - - {initialValues.project_flock?.fcr?.fcr_std != null - ? `${formatNumber(initialValues.project_flock?.fcr?.fcr_std)} g` - : '-'} -
Feed Intake (g) @@ -3273,62 +3165,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - {/* FCR Standard Modal */} - -
- {/* Modal Header */} -
-
- -

Detail Standard FCR

-
- -
-
- {isLoadingFcrStandards ? ( -
- -
- ) : fcrStandards.length > 0 ? ( - - data={fcrStandards} - columns={fcrStandardColumns} - pageSize={100} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - paginationClassName: 'hidden', - }} - /> - ) : ( -

- Tidak ada data FCR standards -

- )} -
-
-
- {/* Production Standard Modal */} Date: Mon, 9 Feb 2026 10:26:23 +0700 Subject: [PATCH 020/149] chore(FE): Refactor tables to remove unused props and imports --- .../table-view/DeliveryOrderProductTable.tsx | 1 - .../table-view/SalesOrderProductTable.tsx | 120 +----------------- 2 files changed, 2 insertions(+), 119 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index af53685c..e6793602 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -32,7 +32,6 @@ const DeliveryOrderProductTable = ({ formType, onEdit, onDelete, - onAddProductClick, marketing, }: DeliveryOrderProductTableProps) => { const onEditRef = useRef(onEdit); diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index a13a1117..15e0f6a2 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -3,15 +3,9 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; -import { - formatCurrency, - formatNumber, - formatVechicleNumber, -} from '@/lib/helper'; +import { formatCurrency, formatNumber } from '@/lib/helper'; import { Icon } from '@iconify/react'; -import { useMemo, useRef } from 'react'; -import * as TanStack from '@tanstack/react-table'; -import CheckboxInput from '@/components/input/CheckboxInput'; +import { useRef } from 'react'; type SalesOrderProductTableProps = { data: SalesOrderProductFormValues[]; @@ -33,116 +27,6 @@ const SalesOrderProductTable = ({ const onEditRef = useRef(onEdit); onEditRef.current = onEdit; - const columns = useMemo( - () => [ - { - id: 'select', - header: ({ - table, - }: { - table: TanStack.Table; - }) => ( -
- -
- ), - cell: ({ row }: { row: TanStack.Row }) => ( -
- -
- ), - }, - { - accessorFn: (row: SalesOrderProductFormValues) => - formatVechicleNumber(row.vehicle_number as string), - header: 'No. Polisi', - }, - { - accessorFn: (row: SalesOrderProductFormValues) => row.kandang?.label, - header: 'Kandang', - }, - { - accessorFn: (row: SalesOrderProductFormValues) => - row.product_warehouse?.label, - header: 'Produk', - }, - { - accessorFn: (row: SalesOrderProductFormValues) => - formatCurrency(parseFloat(row.unit_price as string)), - header: 'Harga Satuan (Rp)', - }, - { - accessorFn: (row: SalesOrderProductFormValues) => - formatNumber(parseFloat(row.total_weight as string), undefined, 0, 5), - header: 'Total Bobot (Kg)', - }, - { - accessorFn: (row: SalesOrderProductFormValues) => - formatNumber(parseFloat(row.qty as string)), - header: 'Kuantitas', - cell: ({ row }: { row: TanStack.Row }) => - formatNumber( - parseFloat(row.original.qty as string), - undefined, - 0, - 5 - ) + - ' ' + - (row.original.uom ?? ''), - }, - { - accessorFn: (row: SalesOrderProductFormValues) => - formatNumber(parseFloat(row.avg_weight as string), undefined, 0, 5), - header: 'Avg. Bobot (Kg)', - }, - { - accessorFn: (row: SalesOrderProductFormValues) => - formatCurrency(parseFloat(row.total_price as string)), - header: 'Total Penjualan (Rp)', - }, - { - header: 'Aksi', - cell: ( - props: TanStack.CellContext - ) => ( -
- - -
- ), - }, - ], - [] - ); - return ( <>
From 80c79cc14b818aa4fbe0d86285bd9710ffc30dfd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 10:46:32 +0700 Subject: [PATCH 021/149] refactor(FE): Refactor DeliveryOrderProductTable to optimize conditional rendering --- .../table-view/DeliveryOrderProductTable.tsx | 127 +++++++++--------- 1 file changed, 62 insertions(+), 65 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index e6793602..d4165623 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -97,26 +97,73 @@ const DeliveryOrderProductTable = ({ height={20} /> -
)}
Gudang + {doItem?.warehouse?.name || + item.marketing_product?.product_warehouse_data + ?.warehouse?.name} +
Produk + {item.marketing_product?.product_warehouse?.label} +
Qty + {item.qty + ? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}` + : '-'} +
Avg Bobot + {formatNumber(Number(item.avg_weight))} Kg +
Total Bobot + {formatNumber(Number(item.total_weight))} +
Total Harga Satuan + {formatCurrency(parseFloat(item.unit_price as string))} +
Total Penjualan + {formatCurrency(parseFloat(item.total_price as string))} +
+ Label + +
+
Value
+
+
Gudang - {doItem?.warehouse?.name || - item.marketing_product?.product_warehouse_data - ?.warehouse?.name} -
Produk - {item.marketing_product?.product_warehouse?.label} -
Qty - {item.qty - ? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}` - : '-'} -
Avg Bobot - {item.avg_weight - ? formatNumber( - parseFloat(item.avg_weight as string) - ) + ' Kg' - : '-'} -
Total Bobot - {formatNumber(parseFloat(item.total_weight as string))} -
Total Harga Satuan - {formatCurrency(parseFloat(item.unit_price as string))} -
Total Penjualan - {formatCurrency(parseFloat(item.total_price as string))} -
From 6cbe14b36ea4472567265ea4eee049c8baf0c4fc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 11:00:29 +0700 Subject: [PATCH 022/149] refactor(FE): Refactor SalesOrderProductTable to use renderTableContent helper --- .../table-view/SalesOrderProductTable.tsx | 316 +++++++++--------- 1 file changed, 158 insertions(+), 158 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 15e0f6a2..18f6145b 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -27,168 +27,168 @@ const SalesOrderProductTable = ({ const onEditRef = useRef(onEdit); onEditRef.current = onEdit; + const renderTableContent = (item: SalesOrderProductFormValues) => ( + <> +
+ Label + +
+
Value
+ {formType !== 'success' && ( +
+ + +
+ )} +
+
No. Polisi{item.vehicle_number}
Gudang{item.kandang?.label}
Kategori{item.marketing_type?.label}
Produk{item.product_warehouse?.label}
Tipe Konversi{item.convertion_unit?.label}
Tipe KonversiWeek {item.week}
Total Peti + {item.total_peti} {item.convertion_unit?.label} +
Total Bobot + {item.total_weight + ? formatNumber(parseFloat(item.total_weight as string)) + + ' Kg' + : '0 Kg'} +
Avg Bobot + {item.avg_weight + ? formatNumber(parseFloat(item.avg_weight as string)) + ' Kg' + : '0 Kg'} +
+ {item.marketing_type?.value === 'telur' + ? 'Total Butir Telur' + : 'Qty'} + + {`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`} +
Harga Satuan + {formatCurrency(parseFloat(item.unit_price as string))} +
Total Penjualan + {formatCurrency(parseFloat(item.total_price as string))} +
- - - - - - <> - - - - - - - - - - - - - - - - - {item.marketing_type?.value.toLowerCase() === 'telur' && ( - - - - - )} - {item.marketing_type?.value.toLowerCase() === - 'ayam_pullet' && ( - - - - - )} - {item.convertion_unit?.value.toLowerCase() === 'peti' && ( - - - - - )} - {item.marketing_type?.value.toLowerCase() !== 'trading' && ( - <> - - - - - - - - - - )} - - - - - - - - - - - - - - -
- Label - -
-
Value
- {formType !== 'success' && ( -
- - -
- )} -
-
No. Polisi{item.vehicle_number}
Gudang{item.kandang?.label}
Kategori - {item.marketing_type?.label} -
Produk - {item.product_warehouse?.label} -
Tipe Konversi - {item.convertion_unit?.label} -
Tipe KonversiWeek {item.week}
Total Peti - {item.total_peti} {item.convertion_unit?.label} -
Total Bobot - {item.total_weight - ? formatNumber( - parseFloat(item.total_weight as string) - ) + ' Kg' - : '0 Kg'} -
Avg Bobot - {item.avg_weight - ? formatNumber( - parseFloat(item.avg_weight as string) - ) + ' Kg' - : '0 Kg'} -
- {item.marketing_type?.value === 'telur' - ? 'Total Butir Telur' - : 'Qty'} - - {`${formatNumber(parseFloat(item.qty as string))} ${item.uom || ''}`} -
Harga Satuan - {formatCurrency(parseFloat(item.unit_price as string))} -
Total Penjualan - {formatCurrency(parseFloat(item.total_price as string))} -
- +
+ {formType === 'success' ? ( +
+ + {renderTableContent(item)} +
+
+ ) : ( + + + {renderTableContent(item)} +
+
+ )} +
))} {formType != 'add_deliver' && formType != 'edit_deliver' && From 911136981a4cf006da0c7a2e93e4bb4274dc3ac9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 11:23:49 +0700 Subject: [PATCH 023/149] refactor(FE): Add clickable "Belum diisi" text for delivery forms --- .../form/table-view/DeliveryOrderProductTable.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index d4165623..bade27d0 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -173,6 +173,17 @@ const DeliveryOrderProductTable = ({ {item.delivery_date ? ( formatDate(item.delivery_date, 'DD MMM YYYY') + ) : formType === 'add_delivery' || + formType === 'edit_delivery' || + formType === 'detail' ? ( + { + onEditRef.current(item.id as number, item); + }} + > + Belum diisi + ) : ( Belum diisi )} From b7fd5d3569ce5d89cc9aeb7277fb902f2ff84b0e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 11:25:19 +0700 Subject: [PATCH 024/149] refactor(FE): Add approval modal and handler for marketing approval --- .../marketing/DeliveryOrderFormModal.tsx | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 76c46ebf..38552258 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -111,6 +111,7 @@ const DeliveryOrderFormModal = ({ const successModal = useModal(); const rejectModal = useModal(); const deleteModal = useModal(); + const approveModal = useModal(); const formRef = useRef(null); const textareaRef = useRef(null); @@ -333,6 +334,33 @@ const DeliveryOrderFormModal = ({ refreshApproval(); }; + const approveMarketingHandler = async (notes: string) => { + if (!marketingId) { + toast.error(`Tidak ada data yang valid untuk di approve.`); + approveModal.closeModal(); + return; + } + + const approveMarketingRes = await SalesOrderApi.singleApproval( + Number(marketingId), + 'APPROVED', + notes + ); + + if (isResponseSuccess(approveMarketingRes)) { + approveModal.closeModal(); + toast.success(approveMarketingRes?.message as string); + closeModalHandler(); + router.push('/marketing'); + } + if (isResponseError(approveMarketingRes)) { + approveModal.closeModal(); + toast.error(approveMarketingRes?.message as string); + } + refreshMarketing(); + refreshApproval(); + }; + const deleteClickHandler = () => { deleteModal.openModal(); }; @@ -723,7 +751,15 @@ const DeliveryOrderFormModal = ({ type='button' color='primary' onClick={() => { - formRef.current?.requestSubmit(); + // Jika masih di step 1 approval, gunakan single approval API + if ( + marketing?.data?.latest_approval?.step_number === 1 + ) { + approveModal.openModal(); + } else { + // Jika sudah di step 2/3, gunakan form submit (delivery products) + formRef.current?.requestSubmit(); + } }} className='p-3 shadow-button-soft text-base-100 rounded-lg text-sm font-semibold' disabled={deliveryRejected} @@ -795,6 +831,21 @@ const DeliveryOrderFormModal = ({ onClick: confirmationModalDeleteClickHandler, }} /> + + ); }; From 1dc6ffca5c81bfe143198bee1e5099ca7d37dbfa Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 11:36:41 +0700 Subject: [PATCH 025/149] refactor(FE): Handle modal action to set step and selected delivery product --- .../marketing/DeliveryOrderFormModal.tsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 38552258..1d3c0dc2 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -491,7 +491,26 @@ const DeliveryOrderFormModal = ({ ); formik.setValues(filledInitialValues); - setStep(1); + + if (modalAction === 'add_delivery') { + // add delivery + const firstDeliveryItem = filledInitialValues.delivery_order?.[0]; + if (firstDeliveryItem) { + setSelectedDeliveryProduct(firstDeliveryItem); + } + setStep(2); // Langsung ke form delivery + } else if (modalAction === 'edit_delivery') { + // edit delivery + const firstDeliveryItem = filledInitialValues.delivery_order?.[0]; + if (firstDeliveryItem) { + setSelectedDeliveryProduct(firstDeliveryItem); + setStep(2); // Langsung ke form edit + } else { + setStep(1); // Jika belum ada data, tampilkan detail view + } + } else { + setStep(1); // Detail view + } } if (isResponseError(marketing)) { @@ -502,7 +521,7 @@ const DeliveryOrderFormModal = ({ }; getFilledInitialValues(); - }, [marketingId, marketing]); + }, [marketingId, marketing, modalAction]); // Reset error message when step changes useEffect(() => { From 862cf38f92e9919e00d8ab79d840d37d92928f6e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 11:41:08 +0700 Subject: [PATCH 026/149] refactor(FE): Refactor DeliveryOrderProductTable to improve readability --- .../table-view/DeliveryOrderProductTable.tsx | 358 +++++++++--------- 1 file changed, 183 insertions(+), 175 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index bade27d0..7d64da83 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -19,6 +19,7 @@ type DeliveryOrderProductTableProps = { | 'detail' | 'rejected' | 'pending' + | 'success' | string | null; marketing?: Marketing; @@ -41,186 +42,193 @@ const DeliveryOrderProductTable = ({ const approvalStepNumber = marketing?.latest_approval?.step_number; + const renderTableContent = (item: DeliveryOrderProductFormValues) => { + const doItem = marketing?.delivery_order?.find( + (doItem) => doItem.do_number === item.do_number + ); + + return ( + <> + + + Label + + +
+
Value
+ {formType !== 'success' && + (formType === 'add_delivery' || + formType === 'edit_delivery' || + formType === 'detail') && ( +
+ +
+ )} +
+ + + <> + + Gudang + + {doItem?.warehouse?.name || + item.marketing_product?.product_warehouse_data?.warehouse?.name} + + + + Produk + + {item.marketing_product?.product_warehouse?.label} + + + + Qty + + {item.qty + ? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}` + : '-'} + + + {Number(item.avg_weight ?? 0) > 0 && ( + + Avg Bobot + + {formatNumber(Number(item.avg_weight))} Kg + + + )} + {Number(item.total_weight ?? 0) > 0 && ( + + Total Bobot + + {formatNumber(Number(item.total_weight))} + + + )} + + Total Harga Satuan + + {formatCurrency(parseFloat(item.unit_price as string))} + + + + Total Penjualan + + {formatCurrency(parseFloat(item.total_price as string))} + + + + + + Label + + +
+
Value
+
+ + + <> + {approvalStepNumber !== 1 && ( + + Tanggal Pengiriman + + {item.delivery_date ? ( + formatDate(item.delivery_date, 'DD MMM YYYY') + ) : formType === 'add_delivery' || + formType === 'edit_delivery' || + formType === 'detail' ? ( + { + onEditRef.current(item.id as number, item); + }} + > + Belum diisi + + ) : ( + Belum diisi + )} + + + )} + {item.do_number && ( + + No. Pengiriman + {item.do_number} + + )} + + No. Polisi + {item.vehicle_number} + + {doItem && ( + + Dokumen Pengiriman + + + + + )} + + + ); + }; + return ( <>
- {data.map((item) => { - const doItem = marketing?.delivery_order?.find( - (doItem) => doItem.do_number === item.do_number - ); - return ( - - ( +
+ {formType === 'success' ? ( +
+
+ {renderTableContent(item)} +
+
+ ) : ( + - - - - Label - - -
-
Value
- {(formType === 'add_delivery' || - formType === 'edit_delivery' || - formType === 'detail') && ( -
- -
- )} -
- - - <> - - Gudang - - {doItem?.warehouse?.name || - item.marketing_product?.product_warehouse_data - ?.warehouse?.name} - - - - Produk - - {item.marketing_product?.product_warehouse?.label} - - - - Qty - - {item.qty - ? `${formatNumber(parseFloat(item.qty as string))} ${item.marketing_product?.uom ?? ''}` - : '-'} - - - {Number(item.avg_weight ?? 0) > 0 && ( - - Avg Bobot - - {formatNumber(Number(item.avg_weight))} Kg - - - )} - {Number(item.total_weight ?? 0) > 0 && ( - - Total Bobot - - {formatNumber(Number(item.total_weight))} - - - )} - - Total Harga Satuan - - {formatCurrency(parseFloat(item.unit_price as string))} - - - - Total Penjualan - - {formatCurrency(parseFloat(item.total_price as string))} - - - - - - Label - - -
-
Value
-
- - - <> - {approvalStepNumber !== 1 && ( - - - Tanggal Pengiriman - - - {item.delivery_date ? ( - formatDate(item.delivery_date, 'DD MMM YYYY') - ) : formType === 'add_delivery' || - formType === 'edit_delivery' || - formType === 'detail' ? ( - { - onEditRef.current(item.id as number, item); - }} - > - Belum diisi - - ) : ( - Belum diisi - )} - - - )} - {item.do_number && ( - - No. Pengiriman - {item.do_number} - - )} - - No. Polisi - - {item.vehicle_number} - - - {doItem && ( - - - Dokumen Pengiriman - - - - - - )} - - - -
- ); - })} + + {renderTableContent(item)} +
+ + )} +
+ ))}
); From ba0753428d397483b4ea186d9674c06d83143edc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 11:49:08 +0700 Subject: [PATCH 027/149] refactor(FE): Fix form reset and selection handling in SalesOrderFormModal --- .../pages/marketing/SalesOrderFormModal.tsx | 11 ++++++++++- .../form/table-view/DeliveryOrderProductTable.tsx | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/SalesOrderFormModal.tsx b/src/components/pages/marketing/SalesOrderFormModal.tsx index 7a15ea59..3d62c516 100644 --- a/src/components/pages/marketing/SalesOrderFormModal.tsx +++ b/src/components/pages/marketing/SalesOrderFormModal.tsx @@ -394,7 +394,7 @@ const SalesOrderFormModal = ({ } formik.setFieldValue('sales_order', updatedProducts); - console.log(formik.values); + setSelectedMarketingProduct(null); nextButtonHandler(); }, [memoSalesOrder, nextButtonHandler] @@ -418,6 +418,15 @@ const SalesOrderFormModal = ({ if (modalAction === 'add' || modalAction === 'edit') { setCurrentModalAction(modalAction); formModal.openModal(); + + if (modalAction === 'add') { + formik.resetForm(); + setStep(1); + setSelectedMarketingProduct(null); + setSelectedDeliveryProduct(null); + setFormErrorMessage(''); + setFormErrorList([]); + } } }, [modalAction]); diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index 7d64da83..12d97b9a 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -49,7 +49,7 @@ const DeliveryOrderProductTable = ({ return ( <> - + Label From bd5b614bf846ed73f6b99350236c1336f5439aa8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 12:01:13 +0700 Subject: [PATCH 028/149] refactor(FE): Fix modal text logic for add_delivery action --- src/components/pages/marketing/DeliveryOrderFormModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 1d3c0dc2..24aec35f 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -796,8 +796,8 @@ const DeliveryOrderFormModal = ({ ref={successModal.ref} iconPosition='left' type='success' - text={`${currentModalAction === 'add' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`} - subtitleText={`${currentModalAction === 'add' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`} + text={`${currentModalAction === 'add_delivery' ? 'Data Berhasil Disimpan' : 'Data Berhasil Diubah'}`} + subtitleText={`${currentModalAction === 'add_delivery' ? 'Data delivery order telah berhasil disimpan.' : 'Data delivery order telah berhasil diubah.'}`} primaryButton={{ text: 'Oke', color: 'primary', From e6a572ac174165601704caada7d6af9ebc828d09 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 13:31:03 +0700 Subject: [PATCH 029/149] refactor(FE): Add API call for updating delivery order on approval step 3 --- .../marketing/DeliveryOrderFormModal.tsx | 79 ++++++++++++++++++- .../delivery-order/DeliverOrderProduct.tsx | 6 +- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 24aec35f..f1d5e3cc 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -408,7 +408,77 @@ const DeliveryOrderFormModal = ({ }, [prevButtonHandler] ); - const handleUpdateDO = useCallback( + + const isApprovalStep3Approved = useMemo(() => { + return ( + isResponseSuccess(marketing) && + marketing.data.latest_approval?.step_number === 3 && + marketing.data.latest_approval?.action === 'APPROVED' + ); + }, [marketing]); + + const handleUpdateDOWithAPI = useCallback( + async (id: number, values: DeliveryOrderProductFormValues) => { + if (!marketingId) { + toast.error('Marketing ID tidak ditemukan'); + return; + } + + setIsLoading(true); + + const updatedDeliveryValues = deliveryOrderValues.map((product) => + product.id === id ? { ...product, ...values } : product + ); + + const payload = { + marketing_id: Number(marketingId), + delivery_products: updatedDeliveryValues + .map((product) => { + if (Boolean(product.delivery_date)) { + return { + marketing_product_id: product.marketing_product_id as number, + unit_price: parseFloat(product.unit_price as string), + total_weight: parseFloat(product.total_weight as string), + qty: parseFloat(product.qty as string), + avg_weight: parseFloat(product.avg_weight as string), + total_price: parseFloat(product.total_price as string), + delivery_date: formatDate( + product.delivery_date as string, + 'yyyy-MM-DD' + ), + vehicle_number: product.vehicle_number, + }; + } + }) + .filter((item) => Boolean(item)), + } as UpdateDeliveryOrderPayload; + + const updateDeliveryRes = await DeliveryOrderApi.update( + Number(marketingId), + payload + ); + + if (isResponseSuccess(updateDeliveryRes)) { + toast.success(updateDeliveryRes?.message as string); + closeModalHandler(); + } + + if (isResponseError(updateDeliveryRes)) { + setFormErrorMessage(updateDeliveryRes?.message as string); + } + + setIsLoading(false); + }, + [ + marketingId, + deliveryOrderValues, + formik.values.sales_order, + prevButtonHandler, + refreshMarketing, + ] + ); + + const handleUpdateDOLocal = useCallback( async (id: number, values: DeliveryOrderProductFormValues) => { setDeliveryOrderValues((prev) => prev.map((product) => @@ -726,7 +796,12 @@ const DeliveryOrderFormModal = ({ exisitingValues={deliveryOrderValues} onSubmitForm={handleAddSubmitDO} initialValues={selectedDeliveryProduct ?? undefined} - onUpdateForm={handleUpdateDO} + onUpdateForm={ + isApprovalStep3Approved + ? handleUpdateDOWithAPI + : handleUpdateDOLocal + } + isLoading={isLoading} /> )} diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index d7b97d8e..5736368a 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -36,6 +36,7 @@ const DeliveryOrderProductForm = ({ exisitingValues, onSubmitForm, onUpdateForm, + isLoading, }: { formState: 'add' | 'edit'; salesOrders: BaseSalesOrder[]; @@ -46,6 +47,7 @@ const DeliveryOrderProductForm = ({ id: number, value: DeliveryOrderProductFormValues ) => Promise; + isLoading?: boolean; }) => { const [formikErrorMessage, setFormErrorMessage] = useState(''); const [selectedProduct, setSelectedProduct] = useState( @@ -793,8 +795,8 @@ const DeliveryOrderProductForm = ({
- - - - - - - - - - - ); -}; - -const FcrsTable = () => { - const { - state: tableFilterState, - updateFilter, - setPage, - setPageSize, - toQueryString: getTableFilterQueryString, - } = useTableFilter({ - initial: { search: '', nameSort: '' }, - paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, - }); - - const { - data: fcrs, - isLoading, - mutate: refreshFcrs, - } = useSWR( - `${FcrApi.basePath}${getTableFilterQueryString()}`, - FcrApi.getAllFetcher - ); - - const deleteModal = useModal(); - - const [selectedFcr, setSelectedFcr] = useState(undefined); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const [sorting, setSorting] = useState([]); - - const fcrsColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - header: 'Aksi', - cell: (props) => { - const currentPageSize = props.table.getPaginationRowModel().rows.length; - const currentPageRows = props.table.getPaginationRowModel().flatRows; - const currentRowRelativeIndex = - currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - - const deleteClickHandler = () => { - setSelectedFcr(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; - - const confirmationModalDeleteClickHandler = async () => { - setIsDeleteLoading(true); - - const deleteResponse = await FcrApi.delete(selectedFcr?.id as number); - - if (isResponseError(deleteResponse)) { - toast.error(deleteResponse.message); - setIsDeleteLoading(false); - return; - } - - refreshFcrs(); - - deleteModal.closeModal(); - toast.success('Successfully delete FCR!'); - setIsDeleteLoading(false); - }; - - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; - - const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - - setPageSize(newVal.value as number); - }; - - // track sorting - useEffect(() => { - const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); - - if (!isNameSorted) { - updateFilter('nameSort', ''); - } else { - updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); - } - }, [sorting]); - - return ( - <> -
-
-
-
- - - -
- - -
- -
- -
-
- - - data={isResponseSuccess(fcrs) ? fcrs?.data : []} - columns={fcrsColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(fcrs) ? fcrs?.meta?.page : 0} - totalItems={isResponseSuccess(fcrs) ? fcrs?.meta?.total_results : 0} - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': isResponseSuccess(fcrs) && fcrs?.data?.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - }} - /> -
- - - - ); -}; - -export default FcrsTable; diff --git a/src/components/pages/master-data/fcr/form/FcrForm.schema.ts b/src/components/pages/master-data/fcr/form/FcrForm.schema.ts deleted file mode 100644 index 21b0b9ee..00000000 --- a/src/components/pages/master-data/fcr/form/FcrForm.schema.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as Yup from 'yup'; - -const FcrStandardSchema: Yup.ObjectSchema<{ - weight: number | string; - fcr_number: number | string; - mortality: number | string; -}> = Yup.object({ - weight: Yup.number().nullable().required('Bobot wajib diisi!'), - fcr_number: Yup.number() - .nullable() - .typeError('FCR harus angka!') - .required('FCR harus diisi!'), - mortality: Yup.number().nullable().required('Mortalitas wajib diisi!'), -}); - -export const FcrFormSchema = Yup.object({ - name: Yup.string().required('Nama wajib diisi!'), - fcrStandards: Yup.array() - .of(FcrStandardSchema) - .min(1, 'Minimal 1 FCR Standard diisi1') - .required('FCR wajib diisi!'), -}); - -export const UpdateFcrFormSchema = FcrFormSchema; - -export type FcrFormValues = Yup.InferType; diff --git a/src/components/pages/master-data/fcr/form/FcrForm.tsx b/src/components/pages/master-data/fcr/form/FcrForm.tsx deleted file mode 100644 index 807e7e45..00000000 --- a/src/components/pages/master-data/fcr/form/FcrForm.tsx +++ /dev/null @@ -1,401 +0,0 @@ -'use client'; - -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { useFormik } from 'formik'; -import { toast } from 'react-hot-toast'; - -import { Icon } from '@iconify/react'; -import Button from '@/components/Button'; -import TextInput from '@/components/input/TextInput'; -import { useModal } from '@/components/Modal'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import RequirePermission from '@/components/helper/RequirePermission'; - -import { - FcrFormSchema, - FcrFormValues, - UpdateFcrFormSchema, -} from '@/components/pages/master-data/fcr/form/FcrForm.schema'; -import { isResponseError } from '@/lib/api-helper'; -import { - CreateFcrPayload, - Fcr, - FcrWithStandards, - UpdateFcrPayload, -} from '@/types/api/master-data/fcr'; -import { FcrApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; -import AlertErrorList from '@/components/helper/form/FormErrors'; -import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; - -interface FcrFormProps { - type?: 'add' | 'edit' | 'detail'; - initialValues?: FcrWithStandards; -} - -const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => { - const router = useRouter(); - const deleteModal = useModal(); - - const [fcrFormErrorMessage, setFcrFormErrorMessage] = useState(''); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const createFcrHandler = useCallback( - async (payload: CreateFcrPayload) => { - const createFcrRes = await FcrApi.create(payload); - - if (isResponseError(createFcrRes)) { - setFcrFormErrorMessage(createFcrRes.message); - return; - } - - toast.success(createFcrRes?.message as string); - router.push('/master-data/fcr'); - }, - [router] - ); - - const updateFcrHandler = useCallback( - async (fcrId: number, payload: UpdateFcrPayload) => { - const updateFcrRes = await FcrApi.update(fcrId, payload); - - if (updateFcrRes?.status === 'error') { - setFcrFormErrorMessage(updateFcrRes.message); - return; - } - - toast.success(updateFcrRes?.message as string); - router.refresh(); - router.push('/master-data/fcr'); - }, - [router] - ); - - const formikInitialValues = useMemo(() => { - return { - name: initialValues?.name ?? '', - fcrStandards: initialValues?.fcr_standards - ? initialValues?.fcr_standards - : [ - { - weight: '', - fcr_number: '', - mortality: '', - }, - ], - }; - }, [initialValues]); - - const formik = useFormik({ - initialValues: formikInitialValues, - validationSchema: type === 'edit' ? UpdateFcrFormSchema : FcrFormSchema, - onSubmit: async (values) => { - setFcrFormErrorMessage(''); - - const fcrPayload: CreateFcrPayload = { - name: values.name, - fcr_standards: values.fcrStandards as CreateFcrPayload['fcr_standards'], - }; - - switch (type) { - case 'add': - await createFcrHandler(fcrPayload); - break; - - case 'edit': - await updateFcrHandler(initialValues?.id as number, fcrPayload); - break; - } - }, - }); - - const { setValues: formikSetValues } = formik; - - const addFcrStandard = () => - formik.setFieldValue('fcrStandards', [ - ...formik.values.fcrStandards, - { - weight: '', - fcr_number: '', - mortality: '', - }, - ]); - - const removeFcrStandard = (i: number) => - formik.setFieldValue( - 'fcrStandards', - formik.values.fcrStandards.filter((_, idx) => idx !== i) - ); - - const deleteFcrClickHandler = () => { - deleteModal.openModal(); - }; - - const confirmationModalDeleteClickHandler = async () => { - setIsDeleteLoading(true); - - await FcrApi.delete(initialValues?.id as number); - - deleteModal.closeModal(); - toast.success('Successfully delete FCR!'); - setIsDeleteLoading(false); - router.push('/master-data/fcr'); - }; - - const isRepeaterInputError = ( - column: keyof CreateFcrPayload['fcr_standards'][0], - idx: number - ) => { - return ( - formik.touched.fcrStandards?.[idx]?.[column] && - Boolean( - formik.errors.fcrStandards?.[idx] instanceof Object && - formik.errors.fcrStandards?.[idx]?.[column] - ) - ); - }; - - useEffect(() => { - formikSetValues(formikInitialValues); - }, [formikSetValues, formikInitialValues]); - - // ===== Formik Error List ===== - const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); - - return ( - <> -
-
- - -

- {type === 'add' && 'Tambah FCR'} - {type === 'edit' && 'Edit FCR'} - {type === 'detail' && 'Detail FCR'} -

-
- -
-
- - -
-
- - - - - - - {type !== 'detail' && } - - - - - {formik.values.fcrStandards.map((fcrStandard, idx) => ( - - - - - {type !== 'detail' && ( - - )} - - ))} - -
BobotFCRMortalitasAksi
- - - - - - - -
-
-
- - {type !== 'detail' && ( - - )} -
- - - -
- {type !== 'add' && ( -
- - - - - {type !== 'edit' && ( - - - - )} -
- )} - - {type !== 'detail' && ( -
- - - -
- )} -
- - {fcrFormErrorMessage && ( -
- - {fcrFormErrorMessage} -
- )} - -
- - {type !== 'add' && ( - - )} - - ); -}; - -export default FcrForm; diff --git a/src/services/api/master-data.ts b/src/services/api/master-data.ts index f15de21d..ea33ba70 100644 --- a/src/services/api/master-data.ts +++ b/src/services/api/master-data.ts @@ -54,11 +54,7 @@ import { CreateBankPayload, UpdateBankPayload, } from '@/types/api/master-data/bank'; -import { - CreateFcrPayload, - Fcr, - UpdateFcrPayload, -} from '@/types/api/master-data/fcr'; + import { CreateFlockPayload, Flock, @@ -131,12 +127,6 @@ export const BankApi = new BaseApiService< UpdateBankPayload >('/master-data/banks'); -export const FcrApi = new BaseApiService< - Fcr, - CreateFcrPayload, - UpdateFcrPayload ->('/master-data/fcrs'); - export const FlockApi = new BaseApiService< Flock, CreateFlockPayload, diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 31a0248d..6bf8c7c6 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -1,5 +1,4 @@ import { Area } from '@/types/api/master-data/area'; -import { Fcr } from '@/types/api/master-data/fcr'; import { Flock } from '@/types/api/master-data/flock'; import { Location } from '@/types/api/master-data/location'; import { Kandang } from '@/types/api/master-data/kandang'; diff --git a/src/types/api/master-data/fcr.d.ts b/src/types/api/master-data/fcr.d.ts deleted file mode 100644 index 45ad25e5..00000000 --- a/src/types/api/master-data/fcr.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BaseMetadata } from '@/types/api/api-general'; - -export type BaseFcr = { - id: number; - name: string; -}; - -export type FcrStandard = { - id: number; - weight: number; - fcr_number: number; - mortality: number; -}; - -export type Fcr = BaseMetadata & BaseFcr; - -export type FcrWithStandards = Fcr & { - fcr_standards: FcrStandard[]; -}; - -export type CreateFcrPayload = { - name: string; - fcr_standards: { - weight: number; - fcr_number: number; - mortality: number; - }[]; -}; - -export type UpdateFcrPayload = CreateFcrPayload; From 89ffad398f218231adac5b2c55dfa9b944e2859f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 14:32:27 +0700 Subject: [PATCH 032/149] refactor(FE): Remove FCR-related links and permissions --- src/config/constant.ts | 6 ------ src/config/route-permission.ts | 5 ----- 2 files changed, 11 deletions(-) diff --git a/src/config/constant.ts b/src/config/constant.ts index fc89763b..66d0af7d 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -220,7 +220,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ 'lti.master.area.list', 'lti.master.banks.list', 'lti.master.customer.list', - 'lti.master.fcr.list', 'lti.master.flocks.list', 'lti.master.kandangs.list', 'lti.master.locations.list', @@ -283,11 +282,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/master-data/nonstock', permission: ['lti.master.nonstocks.list'], }, - { - text: 'FCR', - link: '/master-data/fcr', - permission: ['lti.master.fcr.list'], - }, { text: 'Supplier', link: '/master-data/supplier', diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 20ee5292..dc638b29 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -195,11 +195,6 @@ export const ROUTE_PERMISSIONS: Record = { '/master-data/nonstock/detail/': ['lti.master.nonstocks.detail'], '/master-data/nonstock/detail/edit/': ['lti.master.nonstocks.update'], - '/master-data/fcr/': ['lti.master.fcr.list'], - '/master-data/fcr/add/': ['lti.master.fcr.create'], - '/master-data/fcr/detail/': ['lti.master.fcr.detail'], - '/master-data/fcr/detail/edit/': ['lti.master.fcr.update'], - '/master-data/supplier/': ['lti.master.suppliers.list'], '/master-data/supplier/add/': ['lti.master.suppliers.create'], '/master-data/supplier/detail/': ['lti.master.suppliers.detail'], From 606380460e75cb0b5493fbf3d1bc746e49ab54b3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 15:58:28 +0700 Subject: [PATCH 033/149] feat(FE): Add PDF helper components for badges and typography --- .../helper/pdf/badge/PdfParamBadge.tsx | 25 ++++++ .../helper/pdf/badge/PdfStatusBadge.tsx | 47 +++++++++++ .../helper/pdf/typography/PdfTypography.tsx | 83 +++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/components/helper/pdf/badge/PdfParamBadge.tsx create mode 100644 src/components/helper/pdf/badge/PdfStatusBadge.tsx create mode 100644 src/components/helper/pdf/typography/PdfTypography.tsx diff --git a/src/components/helper/pdf/badge/PdfParamBadge.tsx b/src/components/helper/pdf/badge/PdfParamBadge.tsx new file mode 100644 index 00000000..01fc1a63 --- /dev/null +++ b/src/components/helper/pdf/badge/PdfParamBadge.tsx @@ -0,0 +1,25 @@ +import { Text, View, StyleSheet } from '@react-pdf/renderer'; + +type PdfParamBadgeProps = { + children: React.ReactNode; +}; + +const styles = StyleSheet.create({ + parameterBadge: { + backgroundColor: '#F5F5F5', + color: '#333333', + padding: 4, + borderRadius: 4, + fontSize: 8, + marginRight: 8, + marginBottom: 4, + }, +}); + +export const PdfParamBadge = ({ children }: PdfParamBadgeProps) => { + return ( + + {children} + + ); +}; diff --git a/src/components/helper/pdf/badge/PdfStatusBadge.tsx b/src/components/helper/pdf/badge/PdfStatusBadge.tsx new file mode 100644 index 00000000..e0444284 --- /dev/null +++ b/src/components/helper/pdf/badge/PdfStatusBadge.tsx @@ -0,0 +1,47 @@ +import { Text, View, StyleSheet } from '@react-pdf/renderer'; + +type PdfStatusBadgeProps = { + children: React.ReactNode; + backgroundColor?: string; + textColor?: string; + borderColor?: string; +}; + +const styles = StyleSheet.create({ + statusBadge: { + paddingVertical: 2, + paddingHorizontal: 4, + borderRadius: 12, + fontSize: 7, + fontWeight: 'bold', + borderWidth: 1, + borderStyle: 'solid', + }, + statusBadgeText: { + fontSize: 7, + fontWeight: 'bold', + }, +}); + +export const PdfStatusBadge = ({ + children, + backgroundColor = '#F5F5F5', + textColor = '#333333', + borderColor = '#E5E7EB', +}: PdfStatusBadgeProps) => { + return ( + + + {children} + + + ); +}; diff --git a/src/components/helper/pdf/typography/PdfTypography.tsx b/src/components/helper/pdf/typography/PdfTypography.tsx new file mode 100644 index 00000000..31efe449 --- /dev/null +++ b/src/components/helper/pdf/typography/PdfTypography.tsx @@ -0,0 +1,83 @@ +import { Color } from '@/types/theme'; +import { Text, StyleSheet } from '@react-pdf/renderer'; + +type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label'; + +type TypographyVariant = Color | 'default'; + +type PdfTypographyProps = { + children: React.ReactNode; + size?: TypographySize; + variant?: TypographyVariant; + color?: string; + marginBottom?: number; +}; + +const styles = StyleSheet.create({ + h1: { + fontSize: 14, + fontWeight: 'bold', + marginBottom: 5, + }, + h2: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 8, + }, + h3: { + fontSize: 10, + fontWeight: 'bold', + marginBottom: 4, + }, + h4: { + fontSize: 9, + fontWeight: 'bold', + marginBottom: 3, + }, + p: { + fontSize: 10, + marginBottom: 4, + }, + small: { + fontSize: 8, + marginBottom: 2, + }, + label: { + fontSize: 9, + marginBottom: 5, + }, +}); + +const variantColors: Record = { + default: '#333333', + primary: '#1f74bf', + secondary: '#6B7280', + accent: '#8B5CF6', + neutral: '#6B7280', + info: '#3B82F6', + success: '#065F46', + warning: '#92400E', + error: '#DC2626', + none: '#333333', +}; + +export const PdfTypography = ({ + children, + size = 'p', + variant = 'default', + color, + marginBottom, +}: PdfTypographyProps) => { + const sizeStyle = styles[size]; + const textColor = color || variantColors[variant]; + + const customStyle = { + ...(marginBottom !== undefined && { marginBottom }), + }; + + return ( + + {children} + + ); +}; From c86f0379b5da38b4c7e283ad8a7f8492db8f3301 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 16:01:12 +0700 Subject: [PATCH 034/149] refactor(FE): Refactor CustomerPaymentExportPDF to use reusable PDF components --- .../export/CustomerPaymentExportPDF.tsx | 156 ++++-------------- 1 file changed, 36 insertions(+), 120 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index d132be9a..61e8792a 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -2,7 +2,6 @@ import { Page, - Text, View, Document, StyleSheet, @@ -18,6 +17,9 @@ import { PdfTbodyCell, PdfTfootCell, } from '@/components/helper/pdf/table'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; Font.register({ family: 'Helvetica', @@ -34,53 +36,6 @@ const pdfStyles = StyleSheet.create({ titleSection: { marginBottom: 10, }, - mainTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 5, - color: '#1f74bf', - }, - supplierTitle: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 8, - color: '#1f74bf', - }, - supplierInfo: { - fontSize: 9, - marginBottom: 5, - color: '#333333', - }, - badge: { - backgroundColor: '#1f74bf', - color: '#FFFFFF', - padding: 2, - borderRadius: 2, - fontSize: 7, - fontWeight: 'bold', - alignSelf: 'center', - marginRight: 4, - }, - badgeLunas: { - backgroundColor: '#1f74bf', - color: '#FFFFFF', - }, - badgeBelumLunas: { - backgroundColor: '#F97316', - color: '#FFFFFF', - }, - textError: { - color: '#DC2626', - }, - parameterBadge: { - backgroundColor: '#F5F5F5', - color: '#333333', - padding: 4, - borderRadius: 4, - fontSize: 8, - marginRight: 8, - marginBottom: 4, - }, parameterContainer: { flexDirection: 'row', flexWrap: 'wrap', @@ -100,38 +55,6 @@ interface CustomerPaymentExportPDFParams { }; } -const getParameterText = ( - params?: CustomerPaymentExportPDFParams['params'] -) => { - const paramsText = []; - - if (params?.customer_name) { - paramsText.push(`Customer: ${params.customer_name}`); - } else { - paramsText.push('Semua Customer'); - } - - // TODO: Uncomment when BE is ready - // if (params?.sales) { - // paramsText.push(`Sales: ${params.sales}`); - // } - - if (params?.start_date && params?.end_date) { - const startDate = formatDate(params.start_date, 'DD MMM YYYY'); - const endDate = formatDate(params.end_date, 'DD MMM YYYY'); - paramsText.push(`Periode: ${startDate} - ${endDate}`); - } else if (params?.start_date) { - const startDate = formatDate(params.start_date, 'DD MMM YYYY'); - paramsText.push(`Tanggal: ${startDate}`); - } - - const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm'); - paramsText.push(`Dicetak: ${currentDate}`); - - return paramsText; -}; - -// Helper functions for PdfTable const getTableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, { key: 'trans_date', header: 'Tanggal DO', flex: 1.2, align: 'center' }, @@ -221,15 +144,14 @@ const getTableData = ( { key: 'status', value: item.status ? ( - - {item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'} + + + {item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'} + ) : ( '-' @@ -302,43 +224,37 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { > {/* Title and Parameters */} - + Laporan > Kontrol Pembayaran Customer - + - - - Periode:{' '} - {params.params?.start_date - ? formatDate(params.params.start_date, 'DD MMM YYYY') - : '-'}{' '} - s.d{' '} - {params.params?.end_date - ? formatDate(params.params.end_date, 'DD MMM YYYY') - : '-'} - - + + Periode:{' '} + {params.params?.start_date + ? formatDate(params.params.start_date, 'DD MMM YYYY') + : '-'}{' '} + s.d{' '} + {params.params?.end_date + ? formatDate(params.params.end_date, 'DD MMM YYYY') + : '-'} + {/* TODO: Uncomment when BE is ready */} - {/* - Filter Tanggal: Tanggal DO - */} - - - Customer: {params.params?.customer_name || 'Semua Customer'} - - - - - Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} - - + {/* + Filter Tanggal: Tanggal DO + */} + + Customer: {params.params?.customer_name || 'Semua Customer'} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - + {customerReport.customer.name} - - + + Alamat: {customerReport.customer.address || '-'} - + {/* Table */} From 4cf2f77265c58c2aecbe292911992f3e3331188f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 17:01:28 +0700 Subject: [PATCH 035/149] fix(FE): Fix marketing type value logic in SalesOrderFormModal --- src/components/pages/marketing/SalesOrderFormModal.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/SalesOrderFormModal.tsx b/src/components/pages/marketing/SalesOrderFormModal.tsx index 3d62c516..5c4d6bb2 100644 --- a/src/components/pages/marketing/SalesOrderFormModal.tsx +++ b/src/components/pages/marketing/SalesOrderFormModal.tsx @@ -198,6 +198,13 @@ const SalesOrderFormModal = ({ : 'KG' // termasuk "QTY" dan "KG" : undefined; + // Jika value dari data product ada week, kirim "AYAM_PULLET, jika tidak ada kirim "AYAM" + let marketingTypeValue = + product.marketing_type?.value?.toUpperCase() || ''; + if (marketingTypeValue === 'AYAM,AYAM_PULLET') { + marketingTypeValue = product.week ? 'AYAM_PULLET' : 'AYAM'; + } + return { vehicle_number: product.vehicle_number as string, kandang_id: product.kandang_id as number, @@ -207,8 +214,7 @@ const SalesOrderFormModal = ({ qty: parseFloat(String(product.qty || 0)), avg_weight: parseFloat(String(product.avg_weight || 0)), total_price: parseFloat(String(product.total_price || 0)), - marketing_type: - product.marketing_type?.value?.toUpperCase() || '', + marketing_type: marketingTypeValue, convertion_unit: normalizedConvertionUnit, weight_per_convertion: product.weight_per_convertion ?? undefined, From efec9b62651c30952a635d159cd02610bbc6366a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 17:05:35 +0700 Subject: [PATCH 036/149] feat(FE): Add formatTitleCaseGeneral helper and update usage --- .../pages/marketing/form/MarketingForm.schema.ts | 8 ++++++-- src/lib/helper.ts | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index 17b6d78c..535395ab 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -12,7 +12,11 @@ import { BaseSalesOrder, Marketing, } from '@/types/api/marketing/marketing'; -import { formatDate, formatTitleCase } from '@/lib/helper'; +import { + formatDate, + formatTitleCase, + formatTitleCaseGeneral, +} from '@/lib/helper'; type MarketingSchemaType = { customer_id: number | undefined; @@ -119,7 +123,7 @@ export const SalesProductToFieldValues = ( marketing_type: product.marketing_type ? { value: product.marketing_type, - label: formatTitleCase(product.marketing_type), + label: formatTitleCaseGeneral(product.marketing_type), } : null, convertion_unit: product.convertion_unit diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 665c81f1..2804b7d5 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -45,6 +45,15 @@ export const formatTitleCase = (value: string) => { .join(' '); }; +export const formatTitleCaseGeneral = (value: string) => { + return value + .toLowerCase() + .replace(/_/g, ' ') + .split(' ') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +}; + export function formatVechicleNumber(value: string): string { let result = ''; for (let i = 0; i < (value?.length ?? 0); i++) { From e4e6e563c9518e1f58f4705ce013be7c6519a743 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 17:07:00 +0700 Subject: [PATCH 037/149] refactor(FE): Remove unused helper function `formatTitleCaseGeneral` --- .../pages/marketing/form/MarketingForm.schema.ts | 8 ++------ src/lib/helper.ts | 8 -------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index 535395ab..17b6d78c 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -12,11 +12,7 @@ import { BaseSalesOrder, Marketing, } from '@/types/api/marketing/marketing'; -import { - formatDate, - formatTitleCase, - formatTitleCaseGeneral, -} from '@/lib/helper'; +import { formatDate, formatTitleCase } from '@/lib/helper'; type MarketingSchemaType = { customer_id: number | undefined; @@ -123,7 +119,7 @@ export const SalesProductToFieldValues = ( marketing_type: product.marketing_type ? { value: product.marketing_type, - label: formatTitleCaseGeneral(product.marketing_type), + label: formatTitleCase(product.marketing_type), } : null, convertion_unit: product.convertion_unit diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 2804b7d5..9a802f80 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -38,14 +38,6 @@ export const safeRound = (num: number, decimals: number) => { }; export const formatTitleCase = (value: string) => { - return value - .toLowerCase() - .split(' ') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); -}; - -export const formatTitleCaseGeneral = (value: string) => { return value .toLowerCase() .replace(/_/g, ' ') From bcc2070ed2e7453315a42191964d8fd84a4a4587 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Feb 2026 21:50:35 +0700 Subject: [PATCH 038/149] refactor(FE): Refactor DebtSupplierExportPDF to use reusable PDF components --- .../finance/export/DebtSupllierExportPDF.tsx | 732 +++++------------- 1 file changed, 208 insertions(+), 524 deletions(-) diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index edcd360f..b44c8060 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -2,7 +2,6 @@ import { Page, - Text, View, Document, StyleSheet, @@ -12,6 +11,15 @@ import { import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; import { DebtSupplier } from '@/types/api/report/debt-supplier'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; +import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; +import { + PdfTable, + PdfColumn, + PdfTbodyCell, + PdfTfootCell, +} from '@/components/helper/pdf/table'; Font.register({ family: 'Helvetica', @@ -69,133 +77,6 @@ const pdfStyles = StyleSheet.create({ titleSection: { marginBottom: 10, }, - mainTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 5, - color: '#1f74bf', - }, - supplierTitle: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 8, - color: '#1f74bf', - }, - supplierInfo: { - fontSize: 9, - marginBottom: 5, - color: '#333333', - }, - table: { - borderWidth: 1, - borderColor: '#000000', - marginBottom: 15, - }, - tableRow: { - flexDirection: 'row', - }, - tableHeader: { - backgroundColor: '#F5F5F5', - }, - tableCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - textAlign: 'left', - }, - tableCellNo: { - flex: 0.5, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - textAlign: 'center', - }, - tableCellLast: { - flex: 1, - padding: 4, - fontSize: 7, - }, - tableCellHeader: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - textAlign: 'center', - }, - tableCellHeaderRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'right', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - paddingVertical: 12, - }, - tableCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - textAlign: 'right', - }, - tableCellCenter: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 4, - fontSize: 7, - textAlign: 'center', - }, - tableBorderBottom: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - summaryRow: { - backgroundColor: '#F0F0F0', - fontWeight: 'bold', - }, - badge: { - paddingVertical: 2, - paddingHorizontal: 4, - borderRadius: 12, - fontSize: 5, - fontWeight: 'bold', - borderWidth: 1, - textAlign: 'center', - whiteSpace: 'nowrap', - }, - parameterBadge: { - backgroundColor: '#F5F5F5', - color: '#333333', - padding: 4, - borderRadius: 4, - fontSize: 8, - marginRight: 8, - marginBottom: 4, - }, parameterContainer: { flexDirection: 'row', flexWrap: 'wrap', @@ -203,6 +84,153 @@ const pdfStyles = StyleSheet.create({ }, }); +const getTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'pr_number', header: 'No. PR', flex: 1, align: 'left' }, + { key: 'po_number', header: 'No. PO', flex: 1, align: 'left' }, + { + key: 'received_date', + header: 'Tgl Terima/Bayar', + flex: 0.7, + align: 'center', + }, + { key: 'po_date', header: 'Tgl PO', flex: 0.7, align: 'center' }, + { key: 'aging', header: 'Aging', flex: 0.6, align: 'center' }, + { key: 'area', header: 'Area', flex: 1, align: 'left' }, + { key: 'warehouse', header: 'Gudang', flex: 1, align: 'left' }, + { key: 'due_date', header: 'Jatuh Tempo', flex: 1, align: 'center' }, + { key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'left' }, + { + key: 'total_price', + header: 'Nominal Pembelian (Rp)', + flex: 1.5, + align: 'right', + }, + { + key: 'payment_price', + header: 'Pembayaran (Rp)', + flex: 1.5, + align: 'right', + }, + { + key: 'balance', + header: 'Sisa Saldo Hutang (Rp)', + flex: 1.5, + align: 'right', + }, + { key: 'status', header: 'Status', flex: 1.2, align: 'center' }, + { key: 'travel_number', header: 'No. Perjalanan', flex: 1, align: 'left' }, +]; + +const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => { + return rows.map((item, index) => [ + { key: 'no', value: index + 1 }, + { key: 'pr_number', value: item.pr_number || '-' }, + { key: 'po_number', value: item.po_number || '-' }, + { + key: 'received_date', + value: item.received_date + ? formatDate(item.received_date, 'DD MMM YY') + : '-', + }, + { + key: 'po_date', + value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-', + }, + { + key: 'aging', + value: item.aging != null ? `${formatNumber(item.aging)}` : '-', + }, + { key: 'area', value: item.area?.name || '-' }, + { key: 'warehouse', value: item.warehouse?.name || '-' }, + { + key: 'due_date', + value: item.due_date ? formatDate(item.due_date, 'DD MMM YY') : '-', + }, + { + key: 'due_status', + value: + item.due_status && item.due_status !== '-' ? ( + + {item.due_status} + + ) : ( + '-' + ), + }, + { + key: 'total_price', + value: formatCurrency(item.total_price), + align: 'right', + color: item.total_price < 0 ? 'red' : undefined, + }, + { + key: 'payment_price', + value: formatCurrency(item.payment_price), + align: 'right', + color: item.payment_price < 0 ? 'red' : undefined, + }, + { + key: 'balance', + value: formatCurrency(item.balance), + align: 'right', + color: item.balance < 0 ? 'red' : undefined, + }, + { + key: 'status', + value: + item.status && item.status !== '-' ? ( + + + {item.status} + + + ) : ( + '-' + ), + }, + { key: 'travel_number', value: item.travel_number || '-' }, + ]); +}; + +const getTableFooter = (total: DebtSupplier['total']): PdfTfootCell[] => [ + { key: 'no', value: 'Total' }, + { key: 'pr_number', value: '' }, + { key: 'po_number', value: '' }, + { key: 'received_date', value: '' }, + { key: 'po_date', value: '' }, + { key: 'aging', value: formatNumber(total?.aging || 0) + ' Hari' }, + { key: 'area', value: '' }, + { key: 'warehouse', value: '' }, + { key: 'due_date', value: '' }, + { key: 'due_status', value: '' }, + { + key: 'total_price', + value: formatCurrency(total?.total_price || 0), + align: 'right', + }, + { + key: 'payment_price', + value: formatCurrency(total?.payment_price || 0), + align: 'right', + }, + { + key: 'balance', + value: formatCurrency(total?.debt_price || 0), + align: 'right', + }, + { key: 'status', value: '' }, + { key: 'travel_number', value: '' }, +]; + interface DebtSupplierExportPDFParams { data: DebtSupplier[]; params?: { @@ -219,418 +247,74 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {params.data.map((supplierReport, supplierIndex) => ( {/* Title and Supplier Info */} - + Laporan > Rekapitulasi Hutang ke Supplier - + - - - Periode:{' '} - {params.params?.start_date - ? formatDate(params.params.start_date, 'DD MMM YYYY') - : '-'}{' '} - s.d{' '} - {params.params?.end_date - ? formatDate(params.params.end_date, 'DD MMM YYYY') - : '-'} - - + + Periode:{' '} + {params.params?.start_date + ? formatDate(params.params.start_date, 'DD MMM YYYY') + : '-'}{' '} + s.d{' '} + {params.params?.end_date + ? formatDate(params.params.end_date, 'DD MMM YYYY') + : '-'} + {params.params?.filter_by && ( - - - Filter Tanggal:{' '} - {params.params.filter_by === 'po_date' - ? 'Tanggal PO' - : params.params.filter_by === 'received_date' - ? 'Tanggal Terima' - : params.params.filter_by === 'due_date' - ? 'Tanggal Jatuh Tempo' - : params.params.filter_by} - - + + Filter Tanggal:{' '} + {params.params.filter_by === 'po_date' + ? 'Tanggal PO' + : params.params.filter_by === 'received_date' + ? 'Tanggal Terima' + : params.params.filter_by === 'due_date' + ? 'Tanggal Jatuh Tempo' + : params.params.filter_by} + )} - - - Supplier: {params.params?.supplier_name || 'Semua Supplier'} - - - - - Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} - - + + Supplier: {params.params?.supplier_name || 'Semua Supplier'} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - + {supplierReport.supplier.name} - - + + {supplierReport.supplier.category} - + {/* Table */} - - {/* Table Header */} - - - No - - - No. PR - - - No. PO - - - Tgl Terima/Bayar - - - Tgl PO - - - Aging - - - Area - - - Gudang - - - Jatuh Tempo - - - Status Jatuh Tempo - - - Nominal Pembelian (Rp) - - - Pembayaran (Rp) - - - Sisa Saldo Hutang (Rp) - - - Status - - - No. Perjalanan - - - - {/* Initial Balance Row */} - - - {/* NO */} - - - {/* No. PR */} - - - {/* No. PO */} - - - {/* Tgl Terima/Bayar */} - - - {/* Tgl PO */} - - - {/* Aging */} - - - {/* Area */} - - - {/* Gudang */} - - - {/* Jatuh Tempo */} - - - {/* Status Jatuh Tempo */} - - - {/* Nominal Pembelian (Rp) */} - - - {/* Pembayaran (Rp) */} - - - - {' '} - {/* Sisa Saldo Hutang (Rp) */} - {formatCurrency(supplierReport.initial_balance || 0)} - - - - {/* Status */} - - - - - - - {/* Table Body */} - {supplierReport.rows.map((item, index) => ( - - - {index + 1} - - - {item.pr_number || '-'} - - - {item.po_number || '-'} - - - - {item.received_date - ? item.received_date != '-' - ? formatDate(item.received_date, 'DD MMM YY') - : '-' - : '-'} - - - - - {item.po_date - ? item.po_date != '-' - ? formatDate(item.po_date, 'DD MMM YY') - : '-' - : '-'} - - - - {formatNumber(item.aging)} Hari - - - {item.area?.name || '-'} - - - {item.warehouse?.name || '-'} - - - - {item.due_date - ? item.due_date != '-' - ? formatDate(item.due_date, 'DD MMM YY') - : '-' - : '-'} - - - - {item.due_status && item.due_status !== '-' ? ( - - - {item.due_status} - - - ) : ( - - - )} - - - {formatCurrency(item.total_price)} - - - {formatCurrency(item.payment_price)} - - - {formatCurrency(item.balance)} - - - {item.status && item.status !== '-' ? ( - - - {item.status} - - - ) : ( - - - )} - - - {item.travel_number || '-'} - - - ))} - - {/* Summary Row */} - {supplierReport.total && ( - - - Total - - - - - - - - - - - - - - - {formatNumber(supplierReport.total.aging)} Hari - - - - - - - - - - - - - - - - {formatCurrency(supplierReport.total.total_price)} - - - - - {formatCurrency(supplierReport.total.payment_price)} - - - - {formatCurrency(supplierReport.total.debt_price)} - - - - - - - - - )} - + } + : undefined + } + /> ))} From 80763acc53b8096b44d64fdd5e0c19877b73c2b8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 11:27:43 +0700 Subject: [PATCH 039/149] refactor(FE): Add utility for PDF badge styles and integrate into reports --- src/components/helper/pdf/utils/pdf-badge.ts | 65 +++++++++++++++++++ .../export/CustomerPaymentExportPDF.tsx | 44 ++++++++----- .../export/CustomerPaymentExportXLSX.tsx | 10 +-- .../finance/export/DebtSupllierExportPDF.tsx | 61 ++++------------- 4 files changed, 111 insertions(+), 69 deletions(-) create mode 100644 src/components/helper/pdf/utils/pdf-badge.ts diff --git a/src/components/helper/pdf/utils/pdf-badge.ts b/src/components/helper/pdf/utils/pdf-badge.ts new file mode 100644 index 00000000..4b26b4eb --- /dev/null +++ b/src/components/helper/pdf/utils/pdf-badge.ts @@ -0,0 +1,65 @@ +export type StatusColor = { + bg: string; + text: string; + border: string; +}; + +// Due status colors (for debt supplier reports) +export const dueStatusColors: Record = { + 'SUDAH JATUH TEMPO': { + bg: '#FEE2E2', + text: '#991B1B', + border: '#F87171', + }, // error/red + 'BELUM JATUH TEMPO': { + bg: '#D1FAE5', + text: '#065F46', + border: '#34D399', + }, // success/green + 'MENDEKATI JATUH TEMPO': { + bg: '#FEF3C7', + text: '#92400E', + border: '#FBBF24', + }, // warning/yellow +}; + +// Payment status colors (for customer payment & debt supplier reports) +export const paymentStatusColors: Record = { + 'BELUM LUNAS': { + bg: '#FEF3C7', + text: '#92400E', + border: '#FBBF24', + }, // warning/yellow + LUNAS: { + bg: '#DBEAFE', + text: '#1E40AF', + border: '#60A5FA', + }, // primary/blue + 'PEMBAYARAN SEBAGIAN': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green + PEMBAYARAN: { + bg: '#D1FAE5', + text: '#065F46', + border: '#34D399', + }, // success/green +}; + +// Fallback color for unknown statuses +export const fallbackStatusColor: StatusColor = { + bg: '#F3F4F6', + text: '#374151', + border: '#D1D5DB', +}; // neutral + +export const getPDFBadgeStyle = ( + statusText: string, + type: 'due' | 'payment' = 'payment' +): StatusColor => { + const normalizedStatus = statusText.toUpperCase().trim(); + + const colors = + type === 'due' + ? dueStatusColors[normalizedStatus] + : paymentStatusColors[normalizedStatus]; + + return colors || fallbackStatusColor; +}; diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 61e8792a..1c374e4b 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -9,7 +9,12 @@ import { pdf, } from '@react-pdf/renderer'; -import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; +import { + formatDate, + formatCurrency, + formatNumber, + formatTitleCase, +} from '@/lib/helper'; import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; import { PdfTable, @@ -20,6 +25,7 @@ import { import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; +import { getPDFBadgeStyle } from '@/components/helper/pdf/utils/pdf-badge'; Font.register({ family: 'Helvetica', @@ -70,11 +76,21 @@ const getTableColumns = (): PdfColumn[] => [ { key: 'qty', header: 'Qty', flex: 0.8, align: 'right' }, { key: 'weight', header: 'Berat', flex: 1, align: 'right' }, { key: 'average_weight', header: 'Rata-Rata', flex: 0.8, align: 'right' }, - { key: 'unit_price', header: 'Harga/Unit', flex: 1.2, align: 'right' }, - { key: 'final_price', header: 'Harga Akhir', flex: 1.2, align: 'right' }, - { key: 'total_price', header: 'Total', flex: 1.2, align: 'right' }, - { key: 'payment_amount', header: 'Pembayaran', flex: 1.2, align: 'right' }, - { key: 'accounts_receivable', header: 'Saldo', flex: 1.2, align: 'right' }, + { key: 'unit_price', header: 'Harga/Unit (Rp)', flex: 1.2, align: 'right' }, + { key: 'final_price', header: 'Harga Akhir (Rp)', flex: 1.2, align: 'right' }, + { key: 'total_price', header: 'Total (Rp)', flex: 1.2, align: 'right' }, + { + key: 'payment_amount', + header: 'Pembayaran (Rp)', + flex: 1.2, + align: 'right', + }, + { + key: 'accounts_receivable', + header: 'Saldo (Rp)', + flex: 1.2, + align: 'right', + }, { key: 'status', header: 'Keterangan', flex: 1.5, align: 'center' }, { key: 'pickup_info', header: 'Pengambilan', flex: 1, align: 'left' }, { key: 'sales_person', header: 'Sales', flex: 1.5, align: 'left' }, @@ -139,18 +155,18 @@ const getTableData = ( key: 'accounts_receivable', value: formatCurrency(item.accounts_receivable), align: 'right', - color: item.accounts_receivable < 0 ? '#DC2626' : undefined, + color: item.accounts_receivable < 0 ? 'red' : undefined, }, { key: 'status', value: item.status ? ( - {item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'} + {formatTitleCase(item.status)} ) : ( @@ -204,8 +220,7 @@ const getTableFooter = ( key: 'accounts_receivable', value: formatCurrency(summary?.total_accounts_receivable || 0), align: 'right', - color: - (summary?.total_accounts_receivable || 0) < 0 ? '#DC2626' : undefined, + color: (summary?.total_accounts_receivable || 0) < 0 ? 'red' : undefined, }, { key: 'status', value: '' }, { key: 'pickup_info', value: '' }, @@ -273,8 +288,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { valueKey: 'accounts_receivable', value: customerReport.initial_balance, align: 'right', - color: - customerReport.initial_balance < 0 ? '#DC2626' : 'black', + color: customerReport.initial_balance < 0 ? 'red' : 'black', } : undefined } diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx index 3238d46e..e8bfda5e 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportXLSX.tsx @@ -27,11 +27,11 @@ export const generateCustomerPaymentExcel = async ( { header: 'Ekor/Qty', key: 'qty', width: 10 }, { header: 'Berat (Kg)', key: 'weight', width: 12 }, { header: 'AVG', key: 'avgWeight', width: 10 }, - { header: 'Harga/Unit', key: 'unitPrice', width: 15 }, - { header: 'Harga Akhir', key: 'finalPrice', width: 15 }, - { header: 'Total', key: 'totalPrice', width: 15 }, - { header: 'Pembayaran', key: 'paymentAmount', width: 15 }, - { header: 'Saldo Piutang', key: 'accountsReceivable', width: 15 }, + { header: 'Harga/Unit (Rp)', key: 'unitPrice', width: 15 }, + { header: 'Harga Akhir (Rp)', key: 'finalPrice', width: 15 }, + { header: 'Total (Rp)', key: 'totalPrice', width: 15 }, + { header: 'Pembayaran (Rp)', key: 'paymentAmount', width: 15 }, + { header: 'Saldo Piutang (Rp)', key: 'accountsReceivable', width: 15 }, { header: 'Keterangan', key: 'status', width: 20 }, { header: 'Pengambilan', key: 'pickupInfo', width: 15 }, { header: 'Sales/Marketing', key: 'salesPerson', width: 20 }, diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index b44c8060..b5c26525 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -20,53 +20,13 @@ import { PdfTbodyCell, PdfTfootCell, } from '@/components/helper/pdf/table'; +import { getPDFBadgeStyle } from '@/components/helper/pdf/utils/pdf-badge'; Font.register({ family: 'Helvetica', src: 'helvetica', }); -// Status color mappings (same as in DebtSupplierTab) -const dueStatusColors: Record< - string, - { bg: string; text: string; border: string } -> = { - 'Sudah Jatuh Tempo': { bg: '#FEE2E2', text: '#991B1B', border: '#F87171' }, // error/red - 'Belum Jatuh Tempo': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green - 'Mendekati Jatuh Tempo': { - bg: '#FEF3C7', - text: '#92400E', - border: '#FBBF24', - }, // warning/yellow -}; - -const paymentStatusColors: Record< - string, - { bg: string; text: string; border: string } -> = { - 'Belum Lunas': { bg: '#FEF3C7', text: '#92400E', border: '#FBBF24' }, // warning/yellow - Lunas: { bg: '#DBEAFE', text: '#1E40AF', border: '#60A5FA' }, // primary/blue - Pembayaran: { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green -}; - -/** - * Get badge style for PDF rendering - * @param statusText - The status text - * @param type - Type of status: 'due' or 'payment' - * @returns Style object with background and text colors - */ -const getPDFBadgeStyle = ( - statusText: string, - type: 'due' | 'payment' = 'payment' -) => { - const colors = - type === 'due' - ? dueStatusColors[statusText] - : paymentStatusColors[statusText]; - - return colors || { bg: '#F3F4F6', text: '#374151', border: '#D1D5DB' }; // neutral fallback -}; - const pdfStyles = StyleSheet.create({ page: { fontSize: 10, @@ -99,7 +59,7 @@ const getTableColumns = (): PdfColumn[] => [ { key: 'area', header: 'Area', flex: 1, align: 'left' }, { key: 'warehouse', header: 'Gudang', flex: 1, align: 'left' }, { key: 'due_date', header: 'Jatuh Tempo', flex: 1, align: 'center' }, - { key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'left' }, + { key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'center' }, { key: 'total_price', header: 'Nominal Pembelian (Rp)', @@ -151,13 +111,15 @@ const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => { key: 'due_status', value: item.due_status && item.due_status !== '-' ? ( - - {item.due_status} - + + + {item.due_status} + + ) : ( '-' ), @@ -226,6 +188,7 @@ const getTableFooter = (total: DebtSupplier['total']): PdfTfootCell[] => [ key: 'balance', value: formatCurrency(total?.debt_price || 0), align: 'right', + color: (total?.debt_price || 0) < 0 ? 'red' : undefined, }, { key: 'status', value: '' }, { key: 'travel_number', value: '' }, From 4f9401ed340758270188c9711f2a2f5cb8447b00 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 11:36:27 +0700 Subject: [PATCH 040/149] refactor(FE): Refactor export logic for PurchasesPerSupplier report --- ....tsx => PurchasesPerSupplierExportPDF.tsx} | 0 .../export/PurchasesPerSupplierExportXLSX.tsx | 101 ++++++++++++++++++ .../tab/PurchasesPerSupplierTab.tsx | 92 +--------------- 3 files changed, 105 insertions(+), 88 deletions(-) rename src/components/pages/report/logistic-stock/export/{PurchasesPerSupplierExport.tsx => PurchasesPerSupplierExportPDF.tsx} (100%) create mode 100644 src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx similarity index 100% rename from src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx rename to src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx new file mode 100644 index 00000000..110bd65e --- /dev/null +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX.tsx @@ -0,0 +1,101 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { formatDate, formatCurrency, formatNumber } from '@/lib/helper'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; + +interface PurchasesPerSupplierExportExcelParams { + data: LogisticPurchasePerSupplierReport[]; +} + +export const generatePurchasesPerSupplierExcel = async ( + params: PurchasesPerSupplierExportExcelParams +): Promise => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + const columns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Tanggal Terima', key: 'receiveDate', width: 15 }, + { header: 'Tanggal PO', key: 'poDate', width: 15 }, + { header: 'No. Referensi', key: 'poNumber', width: 15 }, + { header: 'Nama Produk', key: 'productName', width: 30 }, + { header: 'Tujuan', key: 'warehouse', width: 20 }, + { header: 'QTY', key: 'qty', width: 10 }, + { header: 'Harga Beli (Rp)', key: 'unitPrice', width: 18 }, + { header: 'Value Harga Beli (Rp)', key: 'purchaseValue', width: 20 }, + { header: 'Transport (Rp)', key: 'transportUnitPrice', width: 15 }, + { header: 'Value Transport (Rp)', key: 'transportValue', width: 20 }, + { header: 'Jumlah (Rp)', key: 'totalAmount', width: 18 }, + { header: 'Ekspedisi', key: 'expedition', width: 15 }, + { header: 'Surat Jalan', key: 'deliveryNumber', width: 15 }, + ]; + + for (const supplierReport of params.data) { + const supplierData = supplierReport.rows; + const supplierName = supplierReport.supplier?.name || 'Unknown Supplier'; + + const worksheet = workbook.addWorksheet(supplierName.substring(0, 31)); + worksheet.columns = columns; + + supplierData.forEach((item, index) => { + worksheet.addRow({ + no: index + 1, + receiveDate: item.receive_date + ? formatDate(item.receive_date, 'DD MMM YYYY') + : '', + poDate: item.po_date ? formatDate(item.po_date, 'DD MMM YYYY') : '', + poNumber: item.po_number || '', + productName: item.product?.name || '', + warehouse: item.warehouse?.name || '', + qty: formatNumber(item.qty || 0), + unitPrice: formatCurrency(item.unit_price || 0), + purchaseValue: formatCurrency(item.purchase_value || 0), + transportUnitPrice: formatCurrency(item.transport_unit_price || 0), + transportValue: formatCurrency(item.transport_value || 0), + totalAmount: formatCurrency(item.total_amount || 0), + expedition: item.expedition || '', + deliveryNumber: item.delivery_number || '', + }); + }); + + if (supplierReport.summary) { + worksheet.addRow({ + no: 'Total', + receiveDate: '', + poDate: '', + poNumber: '', + productName: '', + warehouse: '', + qty: formatNumber(supplierReport.summary.total_qty || 0), + unitPrice: '', + purchaseValue: formatCurrency( + supplierReport.summary.total_purchase_value || 0 + ), + transportUnitPrice: '', + transportValue: formatCurrency( + supplierReport.summary.total_transport_value || 0 + ), + totalAmount: formatCurrency(supplierReport.summary.total_amount || 0), + expedition: '', + deliveryNumber: '', + }); + } + } + + const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); +}; diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 5366f3cd..1c5a4f2d 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -26,9 +26,9 @@ import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; -import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport'; +import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; +import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX'; import toast from 'react-hot-toast'; -import * as XLSX from 'xlsx'; import { Icon } from '@iconify/react'; const PurchasesPerSupplierTab = () => { @@ -355,98 +355,14 @@ const PurchasesPerSupplierTab = () => { return; } - const workbook = XLSX.utils.book_new(); - - allDataForExport.forEach((supplierReport) => { - const supplierData = supplierReport.rows; - const supplierName = - supplierReport.supplier?.name || 'Unknown Supplier'; - - const excelData: { [key: string]: string | number }[] = - supplierData.map((item, index) => ({ - No: index + 1, - 'Tanggal Terima': item.receive_date - ? formatDate(item.receive_date, 'DD MMM YYYY') - : '', - 'Tanggal PO': item.po_date - ? formatDate(item.po_date, 'DD MMM YYYY') - : '', - 'No. Referensi': item.po_number || '', - 'Nama Produk': item.product?.name || '', - Tujuan: item.warehouse?.name || '', - QTY: item.qty || 0, - 'Harga Beli (Rp)': item.unit_price || 0, - 'Value Harga Beli (Rp)': item.purchase_value || 0, - 'Transport (Rp)': item.transport_unit_price || 0, - 'Value Transport (Rp)': item.transport_value || 0, - 'Jumlah (Rp)': item.total_amount || 0, - Ekspedisi: item.expedition || '', - 'Surat Jalan': item.delivery_number || '', - })); - - if (supplierReport.summary) { - excelData.push({ - No: 'Total', - 'Tanggal Terima': '', - 'Tanggal PO': '', - 'No. Referensi': '', - 'Nama Produk': '', - Tujuan: '', - QTY: supplierReport.summary.total_qty || 0, - 'Harga Beli (Rp)': '', - 'Value Harga Beli (Rp)': - supplierReport.summary.total_purchase_value || 0, - 'Transport (Rp)': '', - 'Value Transport (Rp)': - supplierReport.summary.total_transport_value || 0, - 'Jumlah (Rp)': supplierReport.summary.total_amount || 0, - Ekspedisi: '', - 'Surat Jalan': '', - }); - } - - const worksheet = XLSX.utils.json_to_sheet(excelData); - - const colWidths = [ - { wch: 5 }, // No - { wch: 15 }, // Tanggal Terima - { wch: 15 }, // Tanggal PO - { wch: 15 }, // No. Referensi - { wch: 30 }, // Nama Produk - { wch: 20 }, // Tujuan - { wch: 10 }, // QTY - { wch: 18 }, // Harga Beli - { wch: 20 }, // Value Harga Beli - { wch: 15 }, // Transport - { wch: 20 }, // Value Transport - { wch: 18 }, // Jumlah - { wch: 15 }, // Ekspedisi - { wch: 15 }, // Surat Jalan - ]; - worksheet['!cols'] = colWidths; - - const sheetName = - supplierName.length > 31 - ? supplierName.substring(0, 31) - : supplierName; - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); - }); - - const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; - - XLSX.writeFile(workbook, filename); + await generatePurchasesPerSupplierExcel({ data: allDataForExport }); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); } finally { setIsExcelExportLoading(false); } - }, [ - logisticPurchasePerSupplierExport, - tableFilterState, - areaOptions, - supplierOptions, - ]); + }, [logisticPurchasePerSupplierExport]); const handleExportPdf = useCallback(async () => { setIsPdfExportLoading(true); From def894e5f4a40cf6f67e782615dc69c0f66457f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 11:43:23 +0700 Subject: [PATCH 041/149] refactor(FE): Refactor PDF generation for purchases per supplier --- .../export/PurchasesPerSupplierExportPDF.tsx | 309 +++++++++--------- .../tab/PurchasesPerSupplierTab.tsx | 5 +- 2 files changed, 162 insertions(+), 152 deletions(-) diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx index bd6f301a..cc2b7976 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx @@ -2,7 +2,6 @@ import { Page, - Text, View, Document, StyleSheet, @@ -15,7 +14,11 @@ import { PdfTable, PdfColumn, PdfTbodyCell, + PdfTfootCell, } from '@/components/helper/pdf/table'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; Font.register({ family: 'Helvetica', @@ -32,53 +35,16 @@ const pdfStyles = StyleSheet.create({ titleSection: { marginBottom: 10, }, - mainTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 5, - color: '#1f74bf', - }, - supplierTitle: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 8, - color: '#1f74bf', - }, - badge: { - backgroundColor: '#1f74bf', - color: '#FFFFFF', - padding: 2, - borderRadius: 2, - fontSize: 7, - fontWeight: 'bold', - alignSelf: 'center', - marginRight: 4, - }, - parameterBadge: { - backgroundColor: '#F5F5F5', - color: '#333333', - padding: 4, - borderRadius: 4, - fontSize: 8, - marginRight: 8, - marginBottom: 4, - }, parameterContainer: { flexDirection: 'row', flexWrap: 'wrap', marginBottom: 8, }, - supplierSection: { - marginBottom: 10, - }, - supplierSectionBreak: { - marginBottom: 15, - }, }); interface PurchasesPerSupplierExportParams { data: LogisticPurchasePerSupplierReport[]; - params: { + params?: { area_name?: string; supplier_name?: string; product_name?: string; @@ -92,73 +58,57 @@ interface PurchasesPerSupplierExportParams { }; } -const getParameterText = ( - params: PurchasesPerSupplierExportParams['params'] -) => { - const paramsText = []; - - if (params.supplier_name) { - paramsText.push(`Supplier: ${params.supplier_name}`); - } else { - paramsText.push('Semua Supplier'); - } - - if (params.start_date && params.end_date) { - const startDate = formatDate(params.start_date, 'DD MMM YYYY'); - const endDate = formatDate(params.end_date, 'DD MMM YYYY'); - paramsText.push(`Periode: ${startDate} - ${endDate}`); - } else if (params.start_date) { - const startDate = formatDate(params.start_date, 'DD MMM YYYY'); - paramsText.push(`Tanggal: ${startDate}`); - } - - const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm'); - paramsText.push(`Dicetak: ${currentDate}`); - - return paramsText; -}; - -// Helper functions for PdfTable const getTableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'receive_date', header: 'Tanggal Terima', flex: 1, align: 'center' }, - { key: 'po_date', header: 'Tanggal PO', flex: 1, align: 'center' }, - { key: 'po_number', header: 'Referensi', flex: 1, align: 'left' }, - { key: 'product', header: 'Produk', flex: 1, align: 'left' }, - { key: 'warehouse', header: 'Tujuan', flex: 1, align: 'left' }, - { key: 'qty', header: 'Qty', flex: 0.8, align: 'right' }, - { key: 'unit_price', header: 'Harga Beli', flex: 1.2, align: 'right' }, + { + key: 'receive_date', + header: 'Tanggal Terima', + flex: 1.2, + align: 'center', + }, + { key: 'po_date', header: 'Tanggal PO', flex: 1.2, align: 'center' }, + { key: 'po_number', header: 'No. Referensi', flex: 1.5, align: 'left' }, + { key: 'product', header: 'Nama Produk', flex: 2, align: 'left' }, + { key: 'warehouse', header: 'Tujuan', flex: 1.5, align: 'left' }, + { key: 'qty', header: 'QTY', flex: 0.8, align: 'right' }, + { key: 'unit_price', header: 'Harga Beli (Rp)', flex: 1.5, align: 'right' }, { key: 'purchase_value', - header: 'Nilai Pembelian', - flex: 1.5, + header: 'Value Harga Beli (Rp)', + flex: 1.8, align: 'right', }, { - key: 'transport_price', - header: 'Biaya Transport', - flex: 1.2, + key: 'transport_unit_price', + header: 'Transport (Rp)', + flex: 1.3, align: 'right', }, - { key: 'total_amount', header: 'Total', flex: 1.5, align: 'right' }, - { key: 'expedition', header: 'Armada', flex: 1.2, align: 'center' }, - { key: 'delivery_number', header: 'Surat Jalan', flex: 1, align: 'left' }, + { + key: 'transport_value', + header: 'Value Transport (Rp)', + flex: 1.8, + align: 'right', + }, + { key: 'total_amount', header: 'Jumlah (Rp)', flex: 1.5, align: 'right' }, + { key: 'expedition', header: 'Ekspedisi', flex: 1.2, align: 'center' }, + { key: 'delivery_number', header: 'Surat Jalan', flex: 1.2, align: 'left' }, ]; const getTableData = ( rows: LogisticPurchasePerSupplierReport['rows'] ): PdfTbodyCell[][] => { return rows.map((item, index) => [ - { key: 'no', value: index + 1, align: 'center' }, + { key: 'no', value: index + 1 }, { key: 'receive_date', - value: formatDate(item.receive_date, 'DD-MMM-YYYY'), - align: 'center', + value: item.receive_date + ? formatDate(item.receive_date, 'DD MMM YY') + : '-', }, { key: 'po_date', - value: formatDate(item.po_date, 'DD-MMM-YYYY'), - align: 'center', + value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-', }, { key: 'po_number', value: item.po_number || '-' }, { key: 'product', value: item.product?.name || '-' }, @@ -175,10 +125,15 @@ const getTableData = ( align: 'right', }, { - key: 'transport_price', + key: 'transport_unit_price', value: formatCurrency(item.transport_unit_price || 0), align: 'right', }, + { + key: 'transport_value', + value: formatCurrency(item.transport_value || 0), + align: 'right', + }, { key: 'total_amount', value: formatCurrency(item.total_amount || 0), @@ -186,82 +141,134 @@ const getTableData = ( }, { key: 'expedition', - value: ( - - {item.expedition || '-'} + value: item.expedition ? ( + + + {item.expedition} + + ) : ( + '-' ), - align: 'center', }, { key: 'delivery_number', value: item.delivery_number || '-' }, ]); }; -const createPDFDocument = ( - supplierReports: LogisticPurchasePerSupplierReport[], - params: PurchasesPerSupplierExportParams['params'] -) => ( - - - {/* Title and Parameters */} - - - Laporan > Rekapitulasi Pembelian Per Supplier - - - - - Jenis Tanggal:{' '} - {params.filter_by === 'received_date' - ? 'Tanggal Terima' - : 'Tanggal PO'} - +const getTableFooter = ( + summary: LogisticPurchasePerSupplierReport['summary'] +): PdfTfootCell[] => [ + { key: 'no', value: 'Total' }, + { key: 'receive_date', value: '' }, + { key: 'po_date', value: '' }, + { key: 'po_number', value: '' }, + { key: 'product', value: '' }, + { key: 'warehouse', value: '' }, + { + key: 'qty', + value: formatNumber(summary?.total_qty || 0), + align: 'right', + }, + { key: 'unit_price', value: '' }, + { + key: 'purchase_value', + value: formatCurrency(summary?.total_purchase_value || 0), + align: 'right', + }, + { key: 'transport_unit_price', value: '' }, + { + key: 'transport_value', + value: formatCurrency(summary?.total_transport_value || 0), + align: 'right', + }, + { + key: 'total_amount', + value: formatCurrency(summary?.total_amount || 0), + align: 'right', + }, + { key: 'expedition', value: '' }, + { key: 'delivery_number', value: '' }, +]; + +const createPDFDocument = (params: PurchasesPerSupplierExportParams) => { + return ( + + {params.data.map((supplierReport, supplierIndex) => ( + + {/* Title and Parameters */} + + + Laporan > Rekapitulasi Pembelian Per Supplier + + + + Jenis Tanggal:{' '} + {params.params?.filter_by === 'received_date' + ? 'Tanggal Terima' + : 'Tanggal PO'} + + + Periode:{' '} + {params.params?.start_date + ? formatDate(params.params.start_date, 'DD MMM YYYY') + : '-'}{' '} + s.d{' '} + {params.params?.end_date + ? formatDate(params.params.end_date, 'DD MMM YYYY') + : '-'} + + + Supplier: {params.params?.supplier_name || 'Semua Supplier'} + + + Area: {params.params?.area_name || 'Semua Area'} + + + Produk: {params.params?.product_name || 'Semua Produk'} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + + {supplierReport.supplier.name} + + {supplierReport.supplier.address && ( + + Alamat: {supplierReport.supplier.address} + + )} - {getParameterText(params).map((param, index) => ( - - {param} - - ))} - - - {/* Supplier Sections */} - {supplierReports.map( - ( - supplierReport: LogisticPurchasePerSupplierReport, - supplierIndex: number - ) => { - return ( - - - {supplierReport.supplier.name} - - - - - ); - } - )} - - -); + {/* Table */} + + + ))} + + ); +}; export const generatePurchasesPerSupplierPDF = async ( - data: LogisticPurchasePerSupplierReport[], - params: PurchasesPerSupplierExportParams['params'] + params: PurchasesPerSupplierExportParams ): Promise => { - const PDFDocument = createPDFDocument(data, params); + const PDFDocument = createPDFDocument(params); try { const blob = await pdf(PDFDocument).toBlob(); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 1c5a4f2d..e1659470 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -433,7 +433,10 @@ const PurchasesPerSupplierTab = () => { end_date: tableFilterState.end_date || '', }; - await generatePurchasesPerSupplierPDF(allDataForExport, exportParams); + await generatePurchasesPerSupplierPDF({ + data: allDataForExport, + params: exportParams, + }); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); From d0dea834c1c18b87b72b9b156c4794b0c8f013e3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 11:54:15 +0700 Subject: [PATCH 042/149] refactor(FE): Refactor HppPerKandang export logic to use ExcelJS --- ...gExport.tsx => HppPerkandangExportPDF.tsx} | 0 .../sale/export/HppPerkandangExportXLSX.tsx | 135 ++++++++++++++++++ .../report/sale/tab/HppPerKandangTab.tsx | 132 +---------------- 3 files changed, 142 insertions(+), 125 deletions(-) rename src/components/pages/report/sale/export/{HppPerkandangExport.tsx => HppPerkandangExportPDF.tsx} (100%) create mode 100644 src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx similarity index 100% rename from src/components/pages/report/sale/export/HppPerkandangExport.tsx rename to src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx diff --git a/src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx b/src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx new file mode 100644 index 00000000..20faaa13 --- /dev/null +++ b/src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx @@ -0,0 +1,135 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { formatCurrency, formatNumber } from '@/lib/helper'; +import { + HppPerKandangReport, + HppPerKandangRow, + HppPerKandangPerWeightRange, +} from '@/types/api/report/hpp-per-kandang'; + +interface HppPerKandangExportExcelParams { + data: HppPerKandangReport; + allFeedSuppliers: string; + allDocSuppliers: string; +} + +const formatSuppliers = ( + suppliers: { alias?: string; name: string }[] | null +): string => { + if (!suppliers || suppliers.length === 0) return ''; + return suppliers.map((s) => s.alias || s.name).join(' | '); +}; + +export const generateHppPerKandangExcel = async ( + params: HppPerKandangExportExcelParams +): Promise => { + if (!params.data || !params.data.rows || params.data.rows.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + // ===== REKAPITULASI WORKSHEET ===== + const rekapitulasiColumns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Rentang BW', key: 'weightRange', width: 15 }, + { header: 'Sisa Butir', key: 'eggPieces', width: 15 }, + { header: 'Sisa Kg', key: 'eggKg', width: 12 }, + { header: 'Rata-Rata Bobot (Kg)', key: 'avgWeight', width: 18 }, + { header: 'Feed (Supplier)', key: 'feedSuppliers', width: 20 }, + { header: 'DOC (Supplier)', key: 'docSuppliers', width: 20 }, + { header: 'Rata-Rata Harga DOC', key: 'avgDocPrice', width: 20 }, + { header: 'HPP Telur (Rp/Kg)', key: 'eggHpp', width: 18 }, + { header: 'Nominal Sisa', key: 'eggValue', width: 25 }, + ]; + + const rekapitulasiWorksheet = workbook.addWorksheet('Rekapitulasi'); + rekapitulasiWorksheet.columns = rekapitulasiColumns; + + const perWeightRangeSummary = params.data.summary.per_weight_range || []; + + perWeightRangeSummary.forEach( + (item: HppPerKandangPerWeightRange, index: number) => { + rekapitulasiWorksheet.addRow({ + no: index + 1, + weightRange: item.label || '', + eggPieces: formatNumber(item.egg_production_pieces || 0), + eggKg: formatNumber(item.egg_production_kg || 0), + avgWeight: formatNumber(item.avg_weight_kg || 0), + feedSuppliers: formatSuppliers(item.feed_suppliers), + docSuppliers: formatSuppliers(item.doc_suppliers), + avgDocPrice: formatCurrency(item.average_doc_price_rp || 0), + eggHpp: formatCurrency(item.egg_hpp_rp_per_kg || 0), + eggValue: formatCurrency(item.egg_value_rp || 0), + }); + } + ); + + // ===== DETAIL PER KANDANG WORKSHEET ===== + const detailColumns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Kandang', key: 'kandang', width: 30 }, + { header: 'Rentang Bobot', key: 'weightRange', width: 15 }, + { header: 'Rata-Rata Bobot (KG)', key: 'avgWeightKg', width: 18 }, + { header: 'Sisa Telur (Butir)', key: 'eggPieces', width: 15 }, + { header: 'Sisa Telur (KG)', key: 'eggKg', width: 15 }, + { header: 'Feed (Supplier)', key: 'feedSuppliers', width: 20 }, + { header: 'DOC (Supplier)', key: 'docSuppliers', width: 20 }, + { header: 'Rata-Rata Harga DOC (Rp)', key: 'avgDocPrice', width: 20 }, + { header: 'HPP Telur (Rp/Kg)', key: 'eggHpp', width: 18 }, + { header: 'Nilai Nominal Sisa Telur (Rp)', key: 'eggValue', width: 25 }, + ]; + + const detailWorksheet = workbook.addWorksheet('Detail Per Kandang'); + detailWorksheet.columns = detailColumns; + + const allExportData = params.data.rows; + + allExportData.forEach((item: HppPerKandangRow, index: number) => { + detailWorksheet.addRow({ + no: index + 1, + kandang: item.kandang?.name || '', + weightRange: item.weight_range + ? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}` + : '', + avgWeightKg: formatNumber(item.avg_weight_kg || 0), + eggPieces: formatNumber(item.egg_production_pieces || 0), + eggKg: formatNumber(item.egg_production_kg || 0), + feedSuppliers: formatSuppliers(item.feed_suppliers), + docSuppliers: formatSuppliers(item.doc_suppliers), + avgDocPrice: formatCurrency(item.average_doc_price_rp || 0), + eggHpp: formatCurrency(item.egg_hpp_rp_per_kg || 0), + eggValue: formatCurrency(item.egg_value_rp || 0), + }); + }); + + // Add TOTAL row + const summaryTotal = params.data.summary.total; + detailWorksheet.addRow({ + no: 'TOTAL', + kandang: 'ALL', + weightRange: '-', + avgWeightKg: formatNumber(summaryTotal?.average_weight_kg || 0), + eggPieces: formatNumber(summaryTotal?.total_egg_production_pieces || 0), + eggKg: formatNumber(summaryTotal?.total_egg_production_kg || 0), + feedSuppliers: params.allFeedSuppliers, + docSuppliers: params.allDocSuppliers, + avgDocPrice: formatCurrency(summaryTotal?.total_average_doc_price_rp || 0), + eggHpp: formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0), + eggValue: formatCurrency(summaryTotal?.total_egg_value_rp || 0), + }); + + const filename = `laporan-hpp-harian-kandang-periode-${params.data.period}.xlsx`; + + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); +}; diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 7bd774f3..9e4d4004 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -26,9 +26,9 @@ import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; -import { generateHppPerKandangPDF } from '../export/HppPerkandangExport'; +import { generateHppPerKandangPDF } from '../export/HppPerkandangExportPDF'; +import { generateHppPerKandangExcel } from '../export/HppPerkandangExportXLSX'; import toast from 'react-hot-toast'; -import * as XLSX from 'xlsx'; import { Icon } from '@iconify/react'; const HppPerKandangTab = () => { @@ -346,136 +346,18 @@ const HppPerKandangTab = () => { return; } - const allExportData = - allDataForExport.rows as HppPerKandangReport['rows']; - - const perWeightRangeSummary = - allDataForExport.summary.per_weight_range || []; - - const summaryTotal = allDataForExport.summary.total; - - const rekapitulasiData: { [key: string]: string | number }[] = - perWeightRangeSummary.map( - (item: HppPerKandangPerWeightRange, index: number) => ({ - No: index + 1, - 'Rentang BW': item.label || '', - 'Sisa Butir': item.egg_production_pieces || 0, - 'Sisa Kg': item.egg_production_kg || 0, - 'Rata-Rata Bobot (Kg)': item.avg_weight_kg || 0, - 'Feed (Supplier)': - item.feed_suppliers - ?.map( - (s: { alias?: string; name: string }) => s.alias || s.name - ) - .join(' | ') || '', - 'DOC (Supplier)': - item.doc_suppliers - ?.map( - (s: { alias?: string; name: string }) => s.alias || s.name - ) - .join(' | ') || '', - 'Rata-Rata Harga DOC': item.average_doc_price_rp || 0, - 'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0, - 'Nominal Sisa': item.egg_value_rp || 0, - }) - ); - - const rekapitulasiWorksheet = XLSX.utils.json_to_sheet(rekapitulasiData); - - const rekapitulasiColWidths = [ - { wch: 5 }, // No - { wch: 15 }, // Rentang BW - { wch: 15 }, // Sisa Butir - { wch: 12 }, // Sisa Kg - { wch: 18 }, // Rata-Rata Bobot (Kg) - { wch: 20 }, // Feed (Supplier) - { wch: 20 }, // DOC (Supplier) - { wch: 20 }, // Rata-Rata Harga DOC - { wch: 18 }, // HPP Telur (RP/KG) - { wch: 25 }, // Nominal Sisa - ]; - rekapitulasiWorksheet['!cols'] = rekapitulasiColWidths; - - const excelData: { [key: string]: string | number }[] = allExportData.map( - (item: HppPerKandangRow, index: number) => ({ - No: index + 1, - Kandang: item.kandang?.name || '', - 'Rentang Bobot': item.weight_range - ? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}` - : '', - 'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0, - 'Sisa Telur (Butir)': item.egg_production_pieces || 0, - 'Sisa Telur (KG)': item.egg_production_kg || 0, - 'Feed (Supplier)': - item.feed_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '', - 'DOC (Supplier)': - item.doc_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '', - 'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0, - 'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0, - 'Nilai Nominal Sisa Telur (RP)': item.egg_value_rp || 0, - }) - ); - - excelData.push({ - No: 'TOTAL', - Kandang: 'ALL', - 'Rentang Bobot': '-', - 'Rata-Rata Bobot (KG)': summaryTotal?.average_weight_kg || 0, - 'Sisa Telur (Butir)': summaryTotal?.total_egg_production_pieces || 0, - 'Sisa Telur (KG)': summaryTotal?.total_egg_production_kg || 0, - 'Feed (Supplier)': allFeedSuppliers, - 'DOC (Supplier)': allDocSuppliers, - 'Rata-Rata Harga DOC (RP)': - summaryTotal?.total_average_doc_price_rp || 0, - 'HPP Telur (RP/KG)': summaryTotal?.average_egg_hpp_rp_per_kg || 0, - 'Nilai Nominal Sisa Telur (RP)': summaryTotal?.total_egg_value_rp || 0, + await generateHppPerKandangExcel({ + data: allDataForExport, + allFeedSuppliers, + allDocSuppliers, }); - - const worksheet = XLSX.utils.json_to_sheet(excelData); - - const colWidths = [ - { wch: 5 }, // No - { wch: 30 }, // Kandang - { wch: 15 }, // Rentang Bobot - { wch: 18 }, // Rata-Rata Bobot (KG) - { wch: 15 }, // Sisa Telur (Butir) - { wch: 15 }, // Sisa Telur (KG) - { wch: 20 }, // Feed (Supplier) - { wch: 20 }, // DOC (Supplier) - { wch: 20 }, // Rata-Rata Harga DOC (RP) - { wch: 18 }, // HPP Telur (RP/KG) - { wch: 25 }, // Nilai Nominal Sisa Telur (RP) - ]; - worksheet['!cols'] = colWidths; - - const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet( - workbook, - rekapitulasiWorksheet, - 'Rekapitulasi' - ); - XLSX.utils.book_append_sheet(workbook, worksheet, 'Detail Per Kandang'); - - const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`; - - XLSX.writeFile(workbook, filename); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); } finally { setIsExcelExportLoading(false); } - }, [ - hppPerKandangExport, - tableFilterState, - areaOptions, - locationOptions, - kandangOptions, - ]); + }, [hppPerKandangExport, allFeedSuppliers, allDocSuppliers]); const handleExportPDF = useCallback(async () => { setIsPdfExportLoading(true); From 4775c1e115adcd72c77699f709f12f643d11436f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 12:01:51 +0700 Subject: [PATCH 043/149] refactor(FE): Refactor HppPerKandang PDF generation logic and UI renderer --- .../sale/export/HppPerkandangExportPDF.tsx | 290 ++++++++++-------- .../report/sale/tab/HppPerKandangTab.tsx | 29 +- 2 files changed, 174 insertions(+), 145 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx index 3a76d8f4..94c24e93 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx @@ -2,7 +2,6 @@ import { Page, - Text, View, Document, StyleSheet, @@ -21,6 +20,8 @@ import { PdfTbodyCell, PdfTfootCell, } from '@/components/helper/pdf/table'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; Font.register({ family: 'Helvetica', @@ -37,27 +38,6 @@ const pdfStyles = StyleSheet.create({ titleSection: { marginBottom: 10, }, - mainTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 5, - color: '#1f74bf', - }, - supplierTitle: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 8, - color: '#1f74bf', - }, - parameterBadge: { - backgroundColor: '#F5F5F5', - color: '#333333', - padding: 4, - borderRadius: 4, - fontSize: 8, - marginRight: 8, - marginBottom: 4, - }, parameterContainer: { flexDirection: 'row', flexWrap: 'wrap', @@ -70,7 +50,7 @@ const pdfStyles = StyleSheet.create({ interface HppPerKandangExportParams { data: HppPerKandangReport; - params: { + params?: { area_name?: string; location_name?: string; kandang_name?: string; @@ -82,44 +62,11 @@ interface HppPerKandangExportParams { }; } -const getParameterText = (params: HppPerKandangExportParams['params']) => { - const paramsText = []; - - if (params.area_name && params.area_name !== 'Semua Area') { - paramsText.push(`Area: ${params.area_name}`); - } - - if (params.location_name && params.location_name !== 'Semua Lokasi') { - paramsText.push(`Lokasi: ${params.location_name}`); - } - - if (params.kandang_name && params.kandang_name !== 'Semua Kandang') { - paramsText.push(`Kandang: ${params.kandang_name}`); - } - - if (params.period) { - const formattedDate = formatDate(params.period, 'DD MMM YYYY'); - paramsText.push(`Tanggal: ${formattedDate}`); - } - - if (params.weight_min || params.weight_max) { - const weightRange = - params.weight_min && params.weight_max - ? `${params.weight_min} - ${params.weight_max} kg` - : params.weight_min - ? `≥ ${params.weight_min} kg` - : `≤ ${params.weight_max} kg`; - paramsText.push(`Rentang Bobot: ${weightRange}`); - } - - if (params.show_unrecorded === 'true') { - paramsText.push('Tampilkan: Tanpa Recording'); - } - - const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm'); - paramsText.push(`Dicetak: ${currentDate}`); - - return paramsText; +const formatSuppliers = ( + suppliers: { alias?: string; name: string }[] | null | undefined +): string => { + if (!suppliers || suppliers.length === 0) return '-'; + return suppliers.map((s) => s.alias || s.name).join(' | '); }; // Helper functions for PdfTable - Rekapitulasi @@ -133,65 +80,79 @@ const getRekapitulasiColumns = (): PdfColumn[] => [ flex: 1.2, align: 'right', }, - { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.5, align: 'left' }, - { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1.2, align: 'left' }, + { + key: 'feed_supplier', + header: 'Feed (Supplier)', + flex: 1.5, + align: 'left', + }, + { + key: 'doc_supplier', + header: 'DOC (Supplier)', + flex: 1.2, + align: 'left', + }, { key: 'rata_harga_doc', header: 'Rata-Rata Harga DOC', flex: 1.2, align: 'right', }, - { key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1.2, align: 'right' }, - { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' }, + { + key: 'hpp_telur', + header: 'HPP Telur (Rp/Kg)', + flex: 1.2, + align: 'right', + }, + { + key: 'nominal_sisa', + header: 'Nominal Sisa', + flex: 1.2, + align: 'right', + }, ]; const getRekapitulasiData = ( perWeightRange: HppPerKandangPerWeightRange[] ): PdfTbodyCell[][] => { return perWeightRange.map((group) => [ - { key: 'rentang_bw', value: group.label, align: 'center' }, + { key: 'rentang_bw', value: group.label || '-' }, { key: 'sisa_butir', - value: formatNumber(group.egg_production_pieces), + value: formatNumber(group.egg_production_pieces || 0), align: 'right', }, { key: 'sisa_kg', - value: formatNumber(group.egg_production_kg), + value: formatNumber(group.egg_production_kg || 0), align: 'right', }, { key: 'rata_rata_bobot', - value: formatNumber(group.avg_weight_kg), + value: formatNumber(group.avg_weight_kg || 0), align: 'right', }, { key: 'feed_supplier', - value: - group.feed_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-', + value: formatSuppliers(group.feed_suppliers), }, { key: 'doc_supplier', - value: - group.doc_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-', + value: formatSuppliers(group.doc_suppliers), }, { key: 'rata_harga_doc', - value: formatCurrency(group.average_doc_price_rp), + value: formatCurrency(group.average_doc_price_rp || 0), align: 'right', }, { key: 'hpp_telur', - value: formatCurrency(group.egg_hpp_rp_per_kg), + value: formatCurrency(group.egg_hpp_rp_per_kg || 0), align: 'right', }, { key: 'nominal_sisa', - value: formatCurrency(group.egg_value_rp), + value: formatCurrency(group.egg_value_rp || 0), align: 'right', }, ]); @@ -210,21 +171,45 @@ const getDetailColumns = (): PdfColumn[] => [ }, { key: 'sisa_butir', header: 'Sisa Butir', flex: 0.8, align: 'right' }, { key: 'sisa_kg', header: 'Sisa Kg (Telur)', flex: 0.8, align: 'right' }, - { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.2, align: 'left' }, - { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1, align: 'left' }, + { + key: 'feed_supplier', + header: 'Feed (Supplier)', + flex: 1.2, + align: 'left', + }, + { + key: 'doc_supplier', + header: 'DOC (Supplier)', + flex: 1, + align: 'left', + }, { key: 'rata_harga_doc', header: 'Rata-Rata Harga DOC', flex: 1.2, align: 'right', }, - { key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1, align: 'right' }, - { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' }, + { + key: 'hpp_telur', + header: 'HPP Telur (Rp/Kg)', + flex: 1, + align: 'right', + }, + { + key: 'nominal_sisa', + header: 'Nominal Sisa', + flex: 1.2, + align: 'right', + }, ]; -const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { +const getDetailData = ( + rows: HppPerKandangRow[], + allFeedSuppliers: string, + allDocSuppliers: string +): PdfTbodyCell[][] => { return rows.map((item, index) => [ - { key: 'no', value: index + 1, align: 'center' }, + { key: 'no', value: index + 1 }, { key: 'kandang', value: item.kandang?.name || '-' }, { key: 'rentang_bw', @@ -232,131 +217,146 @@ const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { }, { key: 'rata_rata_bobot', - value: formatNumber(item.avg_weight_kg), + value: formatNumber(item.avg_weight_kg || 0), align: 'right', }, { key: 'sisa_butir', - value: formatNumber(item.egg_production_pieces), + value: formatNumber(item.egg_production_pieces || 0), align: 'right', }, { key: 'sisa_kg', - value: formatNumber(item.egg_production_kg), + value: formatNumber(item.egg_production_kg || 0), align: 'right', }, { key: 'feed_supplier', - value: - item.feed_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-', + value: formatSuppliers(item.feed_suppliers), }, { key: 'doc_supplier', - value: - item.doc_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-', + value: formatSuppliers(item.doc_suppliers), }, { key: 'rata_harga_doc', - value: formatCurrency(item.average_doc_price_rp), + value: formatCurrency(item.average_doc_price_rp || 0), align: 'right', }, { key: 'hpp_telur', - value: formatCurrency(item.egg_hpp_rp_per_kg), + value: formatCurrency(item.egg_hpp_rp_per_kg || 0), align: 'right', }, { key: 'nominal_sisa', - value: formatCurrency(item.egg_value_rp), + value: formatCurrency(item.egg_value_rp || 0), align: 'right', }, ]); }; const getDetailFooter = ( - summary: HppPerKandangReport['summary'] + summary: HppPerKandangReport['summary'], + allFeedSuppliers: string, + allDocSuppliers: string ): PdfTfootCell[] => { if (!summary?.total) return []; - const allFeedSuppliers = - summary.total.feed_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-'; - - const allDocSuppliers = - summary.total.doc_suppliers - ?.map((s: { alias?: string; name: string }) => s.alias || s.name) - .join(' | ') || '-'; - return [ { key: 'no', value: 'TOTAL' }, { key: 'kandang', value: 'ALL' }, { key: 'rentang_bw', value: '-' }, { key: 'rata_rata_bobot', - value: formatNumber(summary.total.average_weight_kg), + value: formatNumber(summary.total.average_weight_kg || 0), align: 'right', }, { key: 'sisa_butir', - value: formatNumber(summary.total.total_egg_production_pieces), + value: formatNumber(summary.total.total_egg_production_pieces || 0), align: 'right', }, { key: 'sisa_kg', - value: formatNumber(summary.total.total_egg_production_kg), + value: formatNumber(summary.total.total_egg_production_kg || 0), align: 'right', }, { key: 'feed_supplier', value: allFeedSuppliers }, { key: 'doc_supplier', value: allDocSuppliers }, { key: 'rata_harga_doc', - value: formatCurrency(summary.total.total_average_doc_price_rp), + value: formatCurrency(summary.total.total_average_doc_price_rp || 0), align: 'right', }, { key: 'hpp_telur', - value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg), + value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg || 0), align: 'right', }, { key: 'nominal_sisa', - value: formatCurrency(summary.total.total_egg_value_rp), + value: formatCurrency(summary.total.total_egg_value_rp || 0), align: 'right', }, ]; }; const createPDFDocument = ( - data: HppPerKandangExportParams['data'], - params: HppPerKandangExportParams['params'] + params: HppPerKandangExportParams, + allFeedSuppliers: string, + allDocSuppliers: string ) => { - const rekapitulasiByWeightRange = data.summary?.per_weight_range || []; + const rekapitulasiByWeightRange = params.data.summary?.per_weight_range || []; + + const weightRangeText = + params.params?.weight_min || params.params?.weight_max + ? params.params.weight_min && params.params.weight_max + ? `${params.params.weight_min} - ${params.params.weight_max} kg` + : params.params.weight_min + ? `≥ ${params.params.weight_min} kg` + : `≤ ${params.params.weight_max} kg` + : '-'; return ( {/* Title and Parameters */} - + Laporan > HPP Harian Kandang - + - {getParameterText(params).map((param, index) => ( - - {param} - - ))} + + Area: {params.params?.area_name || 'Semua Area'} + + + Lokasi: {params.params?.location_name || 'Semua Lokasi'} + + + Kandang: {params.params?.kandang_name || 'Semua Kandang'} + + + Periode:{' '} + {params.params?.period + ? formatDate(params.params.period, 'DD MMM YYYY') + : '-'} + + Rentang Bobot: {weightRangeText} + {params.params?.show_unrecorded === 'true' && ( + Tampilkan: Tanpa Recording + )} + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + {/* Rekapitulasi Section */} - Rekapitulasi + + Rekapitulasi + - Detail Per Kandang + + Detail Per Kandang + @@ -379,10 +393,15 @@ const createPDFDocument = ( }; export const generateHppPerKandangPDF = async ( - data: HppPerKandangExportParams['data'], - params: HppPerKandangExportParams['params'] + params: HppPerKandangExportParams, + allFeedSuppliers: string, + allDocSuppliers: string ): Promise => { - const PDFDocument = createPDFDocument(data, params); + const PDFDocument = createPDFDocument( + params, + allFeedSuppliers, + allDocSuppliers + ); try { const blob = await pdf(PDFDocument).toBlob(); @@ -390,7 +409,8 @@ export const generateHppPerKandangPDF = async ( const link = document.createElement('a'); link.href = url; - const period = params.period || formatDate(new Date(), 'YYYY-MM-DD'); + const period = + params.params?.period || formatDate(new Date(), 'YYYY-MM-DD'); link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`; document.body.appendChild(link); diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 9e4d4004..d9690792 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -406,16 +406,23 @@ const HppPerKandangTab = () => { .join(', ') || 'Semua Kandang' : 'Semua Kandang'; - await generateHppPerKandangPDF(allDataForExport, { - area_name: areaName, - location_name: locationName, - kandang_name: kandangName, - period: tableFilterState.period, - weight_min: tableFilterState.weight_min, - weight_max: tableFilterState.weight_max, - show_unrecorded: tableFilterState.show_unrecorded.toString(), - sort_by: tableFilterState.sort_by, - }); + await generateHppPerKandangPDF( + { + data: allDataForExport, + params: { + area_name: areaName, + location_name: locationName, + kandang_name: kandangName, + period: tableFilterState.period, + weight_min: tableFilterState.weight_min, + weight_max: tableFilterState.weight_max, + show_unrecorded: tableFilterState.show_unrecorded.toString(), + sort_by: tableFilterState.sort_by, + }, + }, + allFeedSuppliers, + allDocSuppliers + ); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { @@ -429,6 +436,8 @@ const HppPerKandangTab = () => { areaOptions, locationOptions, kandangOptions, + allFeedSuppliers, + allDocSuppliers, ]); const getTableColumns = (): ColumnDef[] => { From 2af83bed8a90a125fb379e4f705922075d311d45 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 13:37:34 +0700 Subject: [PATCH 044/149] refactor(FE): Refactor getDetailData to remove unused parameters --- .../report/sale/export/HppPerkandangExportPDF.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx index 94c24e93..d9f33f27 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx @@ -203,11 +203,7 @@ const getDetailColumns = (): PdfColumn[] => [ }, ]; -const getDetailData = ( - rows: HppPerKandangRow[], - allFeedSuppliers: string, - allDocSuppliers: string -): PdfTbodyCell[][] => { +const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { return rows.map((item, index) => [ { key: 'no', value: index + 1 }, { key: 'kandang', value: item.kandang?.name || '-' }, @@ -370,11 +366,7 @@ const createPDFDocument = ( Date: Tue, 10 Feb 2026 14:00:28 +0700 Subject: [PATCH 045/149] refactor(FE): Refactor PDF components to support custom styles --- .../helper/pdf/badge/PdfParamBadge.tsx | 6 ++-- .../helper/pdf/badge/PdfStatusBadge.tsx | 35 +++++++++++-------- .../helper/pdf/typography/PdfTypography.tsx | 11 +++--- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/components/helper/pdf/badge/PdfParamBadge.tsx b/src/components/helper/pdf/badge/PdfParamBadge.tsx index 01fc1a63..fbc2cf2a 100644 --- a/src/components/helper/pdf/badge/PdfParamBadge.tsx +++ b/src/components/helper/pdf/badge/PdfParamBadge.tsx @@ -1,7 +1,9 @@ import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import type { Style } from '@react-pdf/types'; type PdfParamBadgeProps = { children: React.ReactNode; + style?: Style; }; const styles = StyleSheet.create({ @@ -16,9 +18,9 @@ const styles = StyleSheet.create({ }, }); -export const PdfParamBadge = ({ children }: PdfParamBadgeProps) => { +export const PdfParamBadge = ({ children, style }: PdfParamBadgeProps) => { return ( - + {children} ); diff --git a/src/components/helper/pdf/badge/PdfStatusBadge.tsx b/src/components/helper/pdf/badge/PdfStatusBadge.tsx index e0444284..23c9c5e9 100644 --- a/src/components/helper/pdf/badge/PdfStatusBadge.tsx +++ b/src/components/helper/pdf/badge/PdfStatusBadge.tsx @@ -1,10 +1,9 @@ import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import type { Style } from '@react-pdf/types'; type PdfStatusBadgeProps = { children: React.ReactNode; - backgroundColor?: string; - textColor?: string; - borderColor?: string; + style?: Style; }; const styles = StyleSheet.create({ @@ -16,30 +15,38 @@ const styles = StyleSheet.create({ fontWeight: 'bold', borderWidth: 1, borderStyle: 'solid', + backgroundColor: '#F5F5F5', + borderColor: '#E5E7EB', }, statusBadgeText: { fontSize: 7, fontWeight: 'bold', + color: '#333333', }, }); -export const PdfStatusBadge = ({ - children, - backgroundColor = '#F5F5F5', - textColor = '#333333', - borderColor = '#E5E7EB', -}: PdfStatusBadgeProps) => { +export const PdfStatusBadge = ({ children, style }: PdfStatusBadgeProps) => { + const styleRecord = style as Record; + const color = styleRecord?.color as string | undefined; + + const viewStyle = Object.entries(styleRecord || {}).reduce( + (acc, [key, value]) => { + if (key !== 'color') { + acc[key] = value; + } + return acc; + }, + {} as Record + ); + return ( 0 ? [viewStyle as Style] : []), ]} > - + {children} diff --git a/src/components/helper/pdf/typography/PdfTypography.tsx b/src/components/helper/pdf/typography/PdfTypography.tsx index 31efe449..43aac19a 100644 --- a/src/components/helper/pdf/typography/PdfTypography.tsx +++ b/src/components/helper/pdf/typography/PdfTypography.tsx @@ -1,5 +1,6 @@ import { Color } from '@/types/theme'; import { Text, StyleSheet } from '@react-pdf/renderer'; +import type { Style } from '@react-pdf/types'; type TypographySize = 'h1' | 'h2' | 'h3' | 'h4' | 'p' | 'small' | 'label'; @@ -10,7 +11,7 @@ type PdfTypographyProps = { size?: TypographySize; variant?: TypographyVariant; color?: string; - marginBottom?: number; + style?: Style; }; const styles = StyleSheet.create({ @@ -66,17 +67,13 @@ export const PdfTypography = ({ size = 'p', variant = 'default', color, - marginBottom, + style, }: PdfTypographyProps) => { const sizeStyle = styles[size]; const textColor = color || variantColors[variant]; - const customStyle = { - ...(marginBottom !== undefined && { marginBottom }), - }; - return ( - + {children} ); From 5cc51c52d9b1aa1e7d528d6d7c24cebc551075a2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 14:02:53 +0700 Subject: [PATCH 046/149] feat(FE): Add PdfPageNumber component for rendering page numbers --- .../helper/pdf/layout/PdfPageNumber.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/components/helper/pdf/layout/PdfPageNumber.tsx diff --git a/src/components/helper/pdf/layout/PdfPageNumber.tsx b/src/components/helper/pdf/layout/PdfPageNumber.tsx new file mode 100644 index 00000000..977cac89 --- /dev/null +++ b/src/components/helper/pdf/layout/PdfPageNumber.tsx @@ -0,0 +1,48 @@ +import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import type { Style } from '@react-pdf/types'; + +type PdfPageNumberProps = { + style?: Style; + /** + * Format template for page number. + * Use {pageNumber} and {totalPages} as placeholders. + * Default: "{pageNumber} / {totalPages}" + */ + format?: string; +}; + +const styles = StyleSheet.create({ + footer: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + position: 'absolute', + fontSize: 8, + bottom: 30, + left: 0, + right: 0, + textAlign: 'center', + color: 'grey', + }, +}); + +export const PdfPageNumber = ({ + style, + format = '{pageNumber} / {totalPages}', +}: PdfPageNumberProps) => { + return ( + + + format + .replace('{pageNumber}', String(pageNumber)) + .replace('{totalPages}', String(totalPages)) + } + fixed + /> + + ); +}; From 4c6ac6e8e10df7e9fddb14feed4554556c86014a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 14:04:44 +0700 Subject: [PATCH 047/149] refactor(FE): Refactor PdfStatusBadge to use a single style prop --- .../finance/export/CustomerPaymentExportPDF.tsx | 8 +++++--- .../finance/export/DebtSupllierExportPDF.tsx | 16 ++++++++++------ .../export/PurchasesPerSupplierExportPDF.tsx | 8 +++++--- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 1c374e4b..b0cb5c8d 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -162,9 +162,11 @@ const getTableData = ( value: item.status ? ( {formatTitleCase(item.status)} diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index b5c26525..849ce4ef 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -113,9 +113,11 @@ const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => { item.due_status && item.due_status !== '-' ? ( {item.due_status} @@ -148,9 +150,11 @@ const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => { item.status && item.status !== '-' ? ( {item.status} diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx index cc2b7976..3423ca69 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx @@ -144,9 +144,11 @@ const getTableData = ( value: item.expedition ? ( {item.expedition} From be7b2a0f938c8d6e1864c62f37dbdb6255001449 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 16:06:11 +0700 Subject: [PATCH 048/149] refactor(FE): Refactor DailyMarketingReportPDF component for cleaner structure --- .../pages/report/DailyMarketingReportPDF.tsx | 762 ++++++------------ 1 file changed, 228 insertions(+), 534 deletions(-) diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/DailyMarketingReportPDF.tsx index 86ee29bc..53b36b80 100644 --- a/src/components/pages/report/DailyMarketingReportPDF.tsx +++ b/src/components/pages/report/DailyMarketingReportPDF.tsx @@ -1,275 +1,226 @@ 'use client'; -import { - Document, - Image, - Page, - StyleSheet, - Text, - View, -} from '@react-pdf/renderer'; - +import { Page, View, Document, StyleSheet, Font } from '@react-pdf/renderer'; import { DailyMarketingReport, SalesSummary, } from '@/types/api/report/marketing'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + formatCurrency, + formatDate, + formatNumber, + formatTitleCase, +} from '@/lib/helper'; +import { + PdfTable, + PdfColumn, + PdfTbodyCell, + PdfTfootCell, +} from '@/components/helper/pdf/table'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; +import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber'; + +Font.register({ + family: 'Helvetica', + src: 'helvetica', +}); + +const pdfStyles = StyleSheet.create({ + page: { + fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', + }, + titleSection: { + marginBottom: 10, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, +}); interface DailyMarketingReportPDFProps { data?: DailyMarketingReport; total?: SalesSummary; } -const DailyMarketingReportPDFStyle = StyleSheet.create({ - page: { - paddingTop: 24, - paddingBottom: 64, - paddingHorizontal: 16, // Reduce padding to fit more columns - orientation: 'landscape', +const getTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'so_date', header: 'Tanggal Sales Order', flex: 1.3, align: 'center' }, + { + key: 'do_date', + header: 'Tanggal Delivery Order', + flex: 1.3, + align: 'center', }, + { key: 'aging', header: 'Aging (Hari)', flex: 0.7, align: 'center' }, + { key: 'warehouse', header: 'Gudang', flex: 1.2, align: 'left' }, + { key: 'customer', header: 'Pelanggan', flex: 1.5, align: 'left' }, + { key: 'sales', header: 'Sales', flex: 1, align: 'left' }, + { key: 'product', header: 'Produk', flex: 1.3, align: 'left' }, + { key: 'do_number', header: 'Nomor DO', flex: 1.2, align: 'left' }, + { key: 'vehicle', header: 'Nomor Polisi', flex: 1, align: 'left' }, + { key: 'marketing_type', header: 'Tipe Marketing', flex: 1, align: 'center' }, + { key: 'qty', header: 'Quantity', flex: 0.7, align: 'right' }, + { key: 'avg_weight', header: 'Rata-Rata (Kg)', flex: 0.8, align: 'right' }, + { + key: 'total_weight', + header: 'Total Berat (Kg)', + flex: 0.9, + align: 'right', + }, + { key: 'sales_price', header: 'Harga Jual (Rp)', flex: 0.9, align: 'right' }, + { key: 'hpp_price', header: 'HPP (Rp)', flex: 1.3, align: 'right' }, + { key: 'sales_amount', header: 'Total Jual (Rp)', flex: 1, align: 'right' }, + { key: 'hpp_amount', header: 'Total HPP (Rp)', flex: 1.3, align: 'right' }, +]; - companyInfoHeader: { - width: '100%', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - marginBottom: 8, - }, - companyLogo: { - width: 64, - height: 'auto', - }, - companyInfoHeaderDate: { - paddingTop: 8, - fontSize: 10, - }, - companyName: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 4, - }, - companyAddress: { - fontSize: 8, - maxWidth: 400, - marginBottom: 10, - }, +const getTableData = (rows: DailyMarketingReport): PdfTbodyCell[][] => { + return rows.map((row, index) => [ + { key: 'no', value: index + 1 }, + { + key: 'so_date', + value: row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-', + }, + { + key: 'do_date', + value: row.realization_date + ? formatDate(row.realization_date, 'DD MMM YY') + : '-', + }, + { key: 'aging', value: row.aging_days ?? '-' }, + { key: 'warehouse', value: row.warehouse?.name ?? '-' }, + { key: 'customer', value: row.customer?.name ?? '-' }, + { key: 'sales', value: row.sales?.name ?? '-' }, + { key: 'product', value: row.product?.name ?? '-' }, + { key: 'do_number', value: row.do_number ?? '-' }, + { key: 'vehicle', value: row.vehicle_number ?? '-' }, + { + key: 'marketing_type', + value: row.marketing_type ? ( + + + {formatTitleCase(row.marketing_type)} + + + ) : ( + '-' + ), + }, + { key: 'qty', value: formatNumber(row.qty ?? 0), align: 'right' }, + { + key: 'avg_weight', + value: formatNumber(row.average_weight_kg ?? 0), + align: 'right', + }, + { + key: 'total_weight', + value: formatNumber(row.total_weight_kg ?? 0), + align: 'right', + }, + { + key: 'sales_price', + value: formatCurrency(row.sales_price_per_kg ?? 0), + align: 'right', + }, + { + key: 'hpp_price', + value: formatCurrency(row.hpp_price_per_kg ?? 0), + align: 'right', + }, + { + key: 'sales_amount', + value: formatCurrency(row.sales_amount ?? 0), + align: 'right', + }, + { + key: 'hpp_amount', + value: formatCurrency(row.hpp_amount ?? 0), + align: 'right', + }, + ]); +}; - title: { - marginTop: 16, - fontSize: 14, - lineHeight: '150%', - textAlign: 'center', - fontFamily: 'Times-Roman', - fontWeight: 'bold', - }, +const getTableFooter = (summary?: SalesSummary): PdfTfootCell[] => { + if (!summary) return []; - footer: { - width: '100%', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 16, - - position: 'absolute', - fontSize: 8, - bottom: 30, - left: 0, - right: 0, - textAlign: 'center', - color: 'grey', - }, - - // Table Styles - table: { - width: '100%', - marginTop: 16, - borderWidth: 1, - borderColor: '#000000', - borderBottomWidth: 0, - fontSize: 7, // Smaller font for report - }, - tableRow: { - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: '#000000', - alignItems: 'center', - minHeight: 20, - }, - tableHeader: { - backgroundColor: '#f0f0f0', - fontWeight: 'bold', - }, - - // Columns definition (Total 100%) - colNo: { - width: '3%', - padding: 2, - textAlign: 'center', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colSoDate: { - width: '6%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colDoDate: { - width: '6%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colAging: { - width: '3%', - padding: 2, - textAlign: 'center', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colWarehouse: { - width: '7%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colCustomer: { - width: '9%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, // Reduced slightly - colSales: { - width: '6%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colProduct: { - width: '8%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, // Reduced slightly - colDoNumber: { - width: '7%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colVehicle: { - width: '5%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colMarketingType: { - width: '5%', - padding: 2, - textAlign: 'left', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colQty: { - width: '4%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colAvgWeight: { - width: '4%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colTotalWeight: { - width: '5%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colSalesPrice: { - width: '5%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colHppPrice: { - width: '5%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colSalesAmount: { - width: '6%', - padding: 2, - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - }, - colHppAmount: { width: '6%', padding: 2, textAlign: 'right' }, // Last column - - // Text inside columns - cellText: { - fontSize: 6, - }, - headerText: { - fontSize: 7, - fontWeight: 'bold', - textAlign: 'center', - }, - - // Utils - doubleDivider: { - width: '100%', - height: 6, - borderTop: '2px solid black', - borderBottom: '2px solid black', - }, - - // Summary - summaryContainer: { - marginTop: 12, - flexDirection: 'row', - justifyContent: 'flex-end', - width: '100%', - }, - summaryTable: { - width: '30%', - borderWidth: 1, - borderColor: '#000000', - fontSize: 8, - }, - summaryRow: { - flexDirection: 'row', - padding: 2, - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - summaryLabel: { - width: '50%', - fontWeight: 'bold', - }, - summaryValue: { - width: '50%', - textAlign: 'right', - }, -}); + return [ + { key: 'no', value: 'TOTAL' }, + { key: 'so_date', value: '' }, + { key: 'do_date', value: '' }, + { key: 'aging', value: '' }, + { key: 'warehouse', value: '' }, + { key: 'customer', value: '' }, + { key: 'sales', value: '' }, + { key: 'product', value: '' }, + { key: 'do_number', value: '' }, + { key: 'vehicle', value: '' }, + { key: 'marketing_type', value: '' }, + { + key: 'qty', + value: formatNumber(summary.total_qty ?? 0), + align: 'right', + }, + { + key: 'avg_weight', + value: formatNumber(summary.total_weight_kg ?? 0), + align: 'right', + }, + { + key: 'total_weight', + value: formatNumber(summary.total_weight_kg ?? 0), + align: 'right', + }, + { key: 'sales_price', value: '' }, + { + key: 'hpp_price', + value: formatCurrency(summary.total_hpp_price_per_kg ?? 0), + align: 'right', + }, + { + key: 'sales_amount', + value: formatCurrency(summary.total_sales_amount ?? 0), + align: 'right', + }, + { + key: 'hpp_amount', + value: formatCurrency(summary.total_hpp_amount ?? 0), + align: 'right', + }, + ]; +}; const DailyMarketingReportPDF = ({ data, @@ -280,288 +231,31 @@ const DailyMarketingReportPDF = ({ return ( - - - - - - - {formatDate(Date.now(), 'DD MMMM YYYY')} - - - - - - PT LUMBUNG TELUR INDONESIA - - - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 - - - + + {/* Title and Parameters */} + + + Laporan > Penjualan Harian + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - - Laporan Penjualan Harian - + {/* Table */} + - {/* Data Table */} - - {/* Header */} - - - No - - - - Tgl SO - - - - - Tgl DO - - - - Aging - - - - Gudang - - - - - Pelanggan - - - - Sales - - - - Produk - - - - No DO - - - - Plat No - - - - Tipe - - - Qty - - - - Rerata - - - - Berat - - - - Hrg Jual - - - - - HPP/kg - - - - - Total Jual - - - - - Total HPP - - - - - {/* Rows */} - {rows.map((row, index) => ( - - - - {index + 1} - - - - - {formatDate(row.so_date, 'DD/MM/YYYY')} - - - - - {formatDate(row.realization_date, 'DD/MM/YYYY')} - - - - - {row.aging_days} - - - - - {row.warehouse?.name} - - - - - {row.customer?.name} - - - - - {row.sales.name} - - - - - {row.product?.name} - - - - - {row.do_number} - - - - - {row.vehicle_number} - - - - - {row.marketing_type} - - - - - {formatNumber(row.qty)} - - - - - {formatNumber(row.average_weight_kg)} - - - - - {formatNumber(row.total_weight_kg)} - - - - - {formatCurrency(row.sales_price_per_kg)} - - - - - {formatCurrency(row.hpp_price_per_kg)} - - - - - {formatCurrency(row.sales_amount)} - - - - - {formatCurrency(row.hpp_amount)} - - - - ))} - - - {/* Summary */} - - - - - Total Qty: - - - {formatNumber(summary?.total_qty ?? 0)} - - - - - Total Berat (kg): - - - {formatNumber(summary?.total_weight_kg ?? 0)} - - - - - Total Penjualan: - - - {formatCurrency(summary?.total_sales_amount ?? 0)} - - - - - Total HPP Per KG: - - - {formatCurrency(summary?.total_hpp_price_per_kg ?? 0)} - - - - - Total HPP: - - - {formatCurrency(summary?.total_hpp_amount ?? 0)} - - - - - - - - `${pageNumber} / ${totalPages}` - } - fixed - /> - + ); From 5593463eab2ee11713cf82af08e9a6686a4a7c74 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 16:20:19 +0700 Subject: [PATCH 049/149] refactor(FE): Refactor marketing report components into a dedicated folder --- src/app/report/marketing/page.tsx | 2 +- .../{ => marketing}/DailyMarketingsTable.tsx | 0 .../MarketingReportContent.tsx | 4 +- .../export}/DailyMarketingReportPDF.tsx | 0 .../export/HppPerkandangExportPDF.tsx | 0 .../export/HppPerkandangExportXLSX.tsx | 0 .../tab}/DailyMarketingReportContent.tsx | 4 +- .../tab/HppPerKandangTab.tsx | 4 +- .../pages/report/sale/SaleReportTabs.tsx | 37 ------------------- 9 files changed, 7 insertions(+), 44 deletions(-) rename src/components/pages/report/{ => marketing}/DailyMarketingsTable.tsx (100%) rename src/components/pages/report/{ => marketing}/MarketingReportContent.tsx (88%) rename src/components/pages/report/{ => marketing/export}/DailyMarketingReportPDF.tsx (100%) rename src/components/pages/report/{sale => marketing}/export/HppPerkandangExportPDF.tsx (100%) rename src/components/pages/report/{sale => marketing}/export/HppPerkandangExportXLSX.tsx (100%) rename src/components/pages/report/{ => marketing/tab}/DailyMarketingReportContent.tsx (98%) rename src/components/pages/report/{sale => marketing}/tab/HppPerKandangTab.tsx (99%) delete mode 100644 src/components/pages/report/sale/SaleReportTabs.tsx diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx index 52a3d4dd..87ed7a1a 100644 --- a/src/app/report/marketing/page.tsx +++ b/src/app/report/marketing/page.tsx @@ -1,4 +1,4 @@ -import MarketingReportContent from '@/components/pages/report/MarketingReportContent'; +import MarketingReportContent from '@/components/pages/report/marketing/MarketingReportContent'; const MarketingReportPage = () => { return ( diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/marketing/DailyMarketingsTable.tsx similarity index 100% rename from src/components/pages/report/DailyMarketingsTable.tsx rename to src/components/pages/report/marketing/DailyMarketingsTable.tsx diff --git a/src/components/pages/report/MarketingReportContent.tsx b/src/components/pages/report/marketing/MarketingReportContent.tsx similarity index 88% rename from src/components/pages/report/MarketingReportContent.tsx rename to src/components/pages/report/marketing/MarketingReportContent.tsx index 3ebacecb..e38a39d4 100644 --- a/src/components/pages/report/MarketingReportContent.tsx +++ b/src/components/pages/report/marketing/MarketingReportContent.tsx @@ -3,8 +3,8 @@ import { JSX, useState } from 'react'; import Tabs from '@/components/Tabs'; -import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent'; -import HppPerKandangTab from './sale/tab/HppPerKandangTab'; +import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingReportContent'; +import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; type MarketingReportTabType = | 'daily' diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx similarity index 100% rename from src/components/pages/report/DailyMarketingReportPDF.tsx rename to src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx diff --git a/src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx similarity index 100% rename from src/components/pages/report/sale/export/HppPerkandangExportPDF.tsx rename to src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx diff --git a/src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx b/src/components/pages/report/marketing/export/HppPerkandangExportXLSX.tsx similarity index 100% rename from src/components/pages/report/sale/export/HppPerkandangExportXLSX.tsx rename to src/components/pages/report/marketing/export/HppPerkandangExportXLSX.tsx diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx similarity index 98% rename from src/components/pages/report/DailyMarketingReportContent.tsx rename to src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx index 01c360d0..ca5ec12f 100644 --- a/src/components/pages/report/DailyMarketingReportContent.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx @@ -14,9 +14,9 @@ import SelectInput, { } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; -import DailyMarketingsTable from '@/components/pages/report/DailyMarketingsTable'; +import DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import DailyMarketingReportPDF from '@/components/pages/report/DailyMarketingReportPDF'; +import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingReportPDF'; import { Area } from '@/types/api/master-data/area'; import { diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx similarity index 99% rename from src/components/pages/report/sale/tab/HppPerKandangTab.tsx rename to src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index d9690792..c0371abf 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -26,8 +26,8 @@ import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; -import { generateHppPerKandangPDF } from '../export/HppPerkandangExportPDF'; -import { generateHppPerKandangExcel } from '../export/HppPerkandangExportXLSX'; +import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/export/HppPerkandangExportPDF'; +import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; diff --git a/src/components/pages/report/sale/SaleReportTabs.tsx b/src/components/pages/report/sale/SaleReportTabs.tsx deleted file mode 100644 index 988c16b2..00000000 --- a/src/components/pages/report/sale/SaleReportTabs.tsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client'; - -import Tabs from '@/components/Tabs'; -import HppPerKandangTab from '@/components/pages/report/sale/tab/HppPerKandangTab'; - -const SaleReportTabs = () => { - const tabs = [ - // { - // id: '1', - // label: 'Penjualan Harian', - // content: 'Penjualan Harian Tab', - // }, - // { - // id: '2', - // label: 'Transaksi Penjualan DO', - // content: 'Transaksi Penjualan DO Tab', - // }, - // { - // id: '3', - // label: 'Perbandingan HPP Per Rentang BW', - // content: 'Perbandingan HPP Per Rentang BW Tab', - // }, - { - id: '4', - label: 'HPP Harian Kandang', - content: , - }, - ]; - - return ( -
- -
- ); -}; - -export default SaleReportTabs; From 1227b7639ff87983518152f403346b07776ca9f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 16:52:52 +0700 Subject: [PATCH 050/149] refactor(FE): Refactor ProductionResultReportPDF to use reusable PDF components --- .../ProductionResultReportPDF.tsx | 679 ++++++++++-------- 1 file changed, 376 insertions(+), 303 deletions(-) diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx index 9bc27c4b..139f4640 100644 --- a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -1,18 +1,19 @@ 'use client'; import React from 'react'; -import { - Document, - Page, - StyleSheet, - Text, - View, - Image, -} from '@react-pdf/renderer'; +import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer'; import { formatDate, formatNumber } from '@/lib/helper'; import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProductionResult } from '@/types/api/report/production-result'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber'; +import { + PdfTable, + PdfColumn, + PdfTbodyCell, +} from '@/components/helper/pdf/table'; type MappedProductionResultsItem = { projectFlockKandang: BaseProjectFlockKandang; @@ -25,132 +26,28 @@ interface ProductionResultReportPDFProps { const styles = StyleSheet.create({ page: { - paddingTop: 24, - paddingBottom: 52, - paddingHorizontal: 16, - }, - - companyInfoHeader: { - width: '100%', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - marginBottom: 8, - }, - companyLogo: { - width: 64, - height: 'auto', - }, - companyInfoHeaderDate: { - paddingTop: 8, fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', }, - companyName: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 4, - }, - companyAddress: { - fontSize: 8, - maxWidth: 420, + titleSection: { marginBottom: 10, }, - doubleDivider: { - width: '100%', - height: 6, - borderTopWidth: 2, - borderTopColor: '#000', - borderBottomWidth: 2, - borderBottomColor: '#000', - }, - - title: { - marginTop: 14, - fontSize: 14, - lineHeight: '150%', - textAlign: 'center', - fontFamily: 'Times-Roman', - fontWeight: 'bold', - }, - - footer: { - width: '100%', - display: 'flex', + parameterContainer: { flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 16, - position: 'absolute', - fontSize: 8, - bottom: 22, - left: 0, - right: 0, - textAlign: 'center', - color: 'grey', + flexWrap: 'wrap', + marginBottom: 8, }, - - section: { - marginTop: 12, - borderWidth: 1, - borderColor: '#000', - padding: 8, + tableSection: { + marginBottom: 12, }, - - sectionHeader: { - marginBottom: 6, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'baseline', - }, - sectionTitle: { + tableTitle: { fontSize: 10, fontWeight: 'bold', + marginBottom: 6, + color: '#333', }, - sectionSubtitle: { - fontSize: 8, - color: '#444', - }, - - // Simple grid table (label/value pairs) - grid: { - width: '100%', - borderWidth: 1, - borderColor: '#000', - }, - gridRow: { - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: '#000', - }, - gridRowLast: { - borderBottomWidth: 0, - }, - gridCellLabel: { - width: '40%', - paddingVertical: 3, - paddingHorizontal: 6, - fontSize: 8, - borderRightWidth: 1, - borderRightColor: '#000', - fontWeight: 'bold', - }, - gridCellValue: { - width: '60%', - paddingVertical: 3, - paddingHorizontal: 6, - fontSize: 8, - textAlign: 'right', - }, - - // Subsection headings - groupTitle: { - marginTop: 8, - marginBottom: 4, - fontSize: 9, - fontWeight: 'bold', - }, - emptyText: { fontSize: 8, color: '#666', @@ -169,125 +66,243 @@ function valueText(v: unknown) { return String(v); } -/** - * Render label/value table for one ProductionResult. - * Uses a compact grid to keep page readable. - */ -function ProductionResultGrid({ pr }: { pr: ProductionResult }) { - const rows: Array<[string, string]> = [ - ['WOA', valueText(pr.woa)], +// ======================================== +// TABLE 1: WOA & BW +// ======================================== +const getBwTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'woa', header: 'WOA', flex: 0.8, align: 'center' }, + { key: 'bw', header: 'BW', flex: 1, align: 'right' }, + { key: 'std_bw', header: 'Std BW', flex: 1, align: 'right' }, + { key: 'uniformity', header: 'Uniformity', flex: 1.2, align: 'right' }, + { + key: 'std_uniformity', + header: 'Std Uniformity', + flex: 1.3, + align: 'right', + }, +]; - // BW - ['BW', valueText(pr.bw)], - ['Std BW', valueText(pr.std_bw)], - ['Uniformity', valueText(pr.uniformity)], - ['Std Uniformity', valueText(pr.std_uniformity)], +const getBwTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'woa', value: valueText(pr.woa) }, + { key: 'bw', value: valueText(pr.bw), align: 'right' }, + { key: 'std_bw', value: valueText(pr.std_bw), align: 'right' }, + { key: 'uniformity', value: valueText(pr.uniformity), align: 'right' }, + { + key: 'std_uniformity', + value: valueText(pr.std_uniformity), + align: 'right', + }, + ]; + }); +}; - // Dep - ['Dep Kum', valueText(pr.dep_kum)], - ['Dep Std', valueText(pr.dep_std)], +// ======================================== +// TABLE 2: DEPLESI +// ======================================== +const getDepTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'dep_kum', header: 'Dep Kum', flex: 1.5, align: 'right' }, + { key: 'dep_std', header: 'Dep Std', flex: 1.5, align: 'right' }, +]; - // Butiran - ['Butiran Utuh', valueText(pr.butiran_utuh)], - ['Butiran Putih', valueText(pr.butiran_putih)], - ['Butiran Retak', valueText(pr.butiran_retak)], - ['Butiran Pecah', valueText(pr.butiran_pecah)], - ['Butiran Jumlah', valueText(pr.butiran_jumlah)], - ['Total Butir', valueText(pr.total_butir)], +const getDepTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'dep_kum', value: valueText(pr.dep_kum), align: 'right' }, + { key: 'dep_std', value: valueText(pr.dep_std), align: 'right' }, + ]; + }); +}; - // Kg - ['Kg Utuh', valueText(pr.kg_utuh)], - ['Kg Putih', valueText(pr.kg_putih)], - ['Kg Retak', valueText(pr.kg_retak)], - ['Kg Pecah', valueText(pr.kg_pecah)], - ['Kg Jumlah', valueText(pr.kg_jumlah)], - ['Total Kg', valueText(pr.total_kg)], +// ======================================== +// TABLE 3: BUTIRAN +// ======================================== +const getButiranTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'butiran_utuh', header: 'Utuh', flex: 1.2, align: 'right' }, + { key: 'butiran_putih', header: 'Putih', flex: 1.2, align: 'right' }, + { key: 'butiran_retak', header: 'Retak', flex: 1.2, align: 'right' }, + { key: 'butiran_pecah', header: 'Pecah', flex: 1.2, align: 'right' }, + { key: 'butiran_jumlah', header: 'Jumlah', flex: 1.2, align: 'right' }, + { key: 'total_butir', header: 'Total Butir', flex: 1.3, align: 'right' }, +]; - // % - ['% Utuh', valueText(pr.persen_utuh)], - ['% Putih', valueText(pr.persen_putih)], - ['% Retak', valueText(pr.persen_retak)], - ['% Pecah', valueText(pr.persen_pecah)], +const getButiranTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { + key: 'butiran_utuh', + value: valueText(pr.butiran_utuh), + align: 'right', + }, + { + key: 'butiran_putih', + value: valueText(pr.butiran_putih), + align: 'right', + }, + { + key: 'butiran_retak', + value: valueText(pr.butiran_retak), + align: 'right', + }, + { + key: 'butiran_pecah', + value: valueText(pr.butiran_pecah), + align: 'right', + }, + { + key: 'butiran_jumlah', + value: valueText(pr.butiran_jumlah), + align: 'right', + }, + { + key: 'total_butir', + value: valueText(pr.total_butir), + align: 'right', + }, + ]; + }); +}; - // Produksi - ['HD', valueText(pr.hd)], - ['HD Std', valueText(pr.hd_std)], - ['FI', valueText(pr.fi)], - ['FI Std', valueText(pr.fi_std)], - ['EM', valueText(pr.em)], - ['EM Std', valueText(pr.em_std)], - ['EW', valueText(pr.ew)], - ['EW Std', valueText(pr.ew_std)], - ['FCR', valueText(pr.fcr)], - ['FCR Std', valueText(pr.fcr_std)], - ['HH', valueText(pr.hh)], - ['HH Std', valueText(pr.hh_std)], - ]; +// ======================================== +// TABLE 4: BERAT (KG) +// ======================================== +const getKgTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'kg_utuh', header: 'Utuh (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_putih', header: 'Putih (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_retak', header: 'Retak (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_pecah', header: 'Pecah (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_jumlah', header: 'Jumlah (Kg)', flex: 1.3, align: 'right' }, + { key: 'total_kg', header: 'Total (Kg)', flex: 1.3, align: 'right' }, +]; - return ( - - {rows.map(([label, value], idx) => { - const isLast = idx === rows.length - 1; - return ( - - {label} - {value} - - ); - })} - - ); -} +const getKgTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'kg_utuh', value: valueText(pr.kg_utuh), align: 'right' }, + { key: 'kg_putih', value: valueText(pr.kg_putih), align: 'right' }, + { key: 'kg_retak', value: valueText(pr.kg_retak), align: 'right' }, + { key: 'kg_pecah', value: valueText(pr.kg_pecah), align: 'right' }, + { key: 'kg_jumlah', value: valueText(pr.kg_jumlah), align: 'right' }, + { key: 'total_kg', value: valueText(pr.total_kg), align: 'right' }, + ]; + }); +}; -/** - * If there are multiple ProductionResult entries for a kandang, - * we show them sequentially with a small header per result. - * - * You can later change this to render only the latest WOA, or group by week. - */ -function ProductionResultList({ - productionResults, -}: { - productionResults: ProductionResult[]; -}) { - return ( - - {productionResults.map((pr, idx) => { - const kandangName = - pr.project_flock?.kandang?.name || - pr.project_flock?.kandang?.id?.toString() || - ''; +// ======================================== +// TABLE 5: PERSENTASE +// ======================================== +const getPersenTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'persen_utuh', header: '% Utuh', flex: 1.5, align: 'right' }, + { key: 'persen_putih', header: '% Putih', flex: 1.5, align: 'right' }, + { key: 'persen_retak', header: '% Retak', flex: 1.5, align: 'right' }, + { key: 'persen_pecah', header: '% Pecah', flex: 1.5, align: 'right' }, +]; - // Optional: show a compact subheader - const headerLeft = `Data #${idx + 1}`; - const headerRight = - kandangName && pr.woa !== undefined - ? `${kandangName} • WOA ${safeNum(pr.woa)}` - : pr.woa !== undefined - ? `WOA ${safeNum(pr.woa)}` - : ''; +const getPersenTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { + key: 'persen_utuh', + value: valueText(pr.persen_utuh), + align: 'right', + }, + { + key: 'persen_putih', + value: valueText(pr.persen_putih), + align: 'right', + }, + { + key: 'persen_retak', + value: valueText(pr.persen_retak), + align: 'right', + }, + { + key: 'persen_pecah', + value: valueText(pr.persen_pecah), + align: 'right', + }, + ]; + }); +}; - return ( - - - {headerLeft} - {headerRight} - +// ======================================== +// TABLE 6: PRODUKSI (HD, FI, EM, EW) +// ======================================== +const getProduksi1TableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'hd', header: 'HD', flex: 0.8, align: 'right' }, + { key: 'hd_std', header: 'HD Std', flex: 1, align: 'right' }, + { key: 'fi', header: 'FI', flex: 0.8, align: 'right' }, + { key: 'fi_std', header: 'FI Std', flex: 1, align: 'right' }, + { key: 'em', header: 'EM', flex: 0.8, align: 'right' }, + { key: 'em_std', header: 'EM Std', flex: 1, align: 'right' }, + { key: 'ew', header: 'EW', flex: 0.8, align: 'right' }, + { key: 'ew_std', header: 'EW Std', flex: 1, align: 'right' }, +]; - - - ); - })} - - ); -} +const getProduksi1TableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'hd', value: valueText(pr.hd), align: 'right' }, + { key: 'hd_std', value: valueText(pr.hd_std), align: 'right' }, + { key: 'fi', value: valueText(pr.fi), align: 'right' }, + { key: 'fi_std', value: valueText(pr.fi_std), align: 'right' }, + { key: 'em', value: valueText(pr.em), align: 'right' }, + { key: 'em_std', value: valueText(pr.em_std), align: 'right' }, + { key: 'ew', value: valueText(pr.ew), align: 'right' }, + { key: 'ew_std', value: valueText(pr.ew_std), align: 'right' }, + ]; + }); +}; + +// ======================================== +// TABLE 7: PRODUKSI (FCR, HH) +// ======================================== +const getProduksi2TableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'fcr', header: 'FCR', flex: 1, align: 'right' }, + { key: 'fcr_std', header: 'FCR Std', flex: 1.2, align: 'right' }, + { key: 'hh', header: 'HH', flex: 1, align: 'right' }, + { key: 'hh_std', header: 'HH Std', flex: 1.2, align: 'right' }, +]; + +const getProduksi2TableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'fcr', value: valueText(pr.fcr), align: 'right' }, + { key: 'fcr_std', value: valueText(pr.fcr_std), align: 'right' }, + { key: 'hh', value: valueText(pr.hh), align: 'right' }, + { key: 'hh_std', value: valueText(pr.hh_std), align: 'right' }, + ]; + }); +}; /** * ✅ Main PDF Component @@ -297,90 +312,148 @@ const ProductionResultReportPDF = ({ }: ProductionResultReportPDFProps) => { return ( - - {/* Header */} - - - - - {formatDate(Date.now(), 'DD MMMM YYYY')} - + {mappedProductionResults.length === 0 ? ( + + {/* Title and Parameters */} + + + Laporan > Production Result + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + - - PT LUMBUNG TELUR INDONESIA - - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 - - - - - - - Laporan Production Result - - {/* Sections per ProjectFlockKandang */} - {mappedProductionResults.length === 0 ? ( Tidak ada data. - ) : ( - mappedProductionResults.map((item, idx) => { - const pfk = item.projectFlockKandang; - // Try to display meaningful identifiers. - // Adjust these fields based on your real BaseProjectFlockKandang structure. - const kandangName = - pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`; + + + ) : ( + mappedProductionResults.map((item, idx) => { + const pfk = item.projectFlockKandang; - const projectName = pfk?.project_flock?.name ?? ''; + const kandangName = + pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`; - const locationName = pfk?.project_flock?.location?.name ?? ''; + const projectName = pfk?.project_flock?.name ?? ''; - const areaName = pfk?.project_flock?.area?.name ?? ''; + const locationName = pfk?.project_flock?.location?.name ?? ''; - return ( - 0} // each kandang starts on a new page for clarity - > - - - {projectName - ? `${projectName} • ${kandangName}` - : kandangName} - - - {[areaName, locationName].filter(Boolean).join(' • ')} - + const areaName = pfk?.project_flock?.area?.name ?? ''; + + const hasData = + item.productionResult && item.productionResult.length > 0; + + return ( + + {/* Title and Parameters */} + + + Laporan > Production Result + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - - {item.productionResult && item.productionResult.length > 0 ? ( - - ) : ( - - Tidak ada production result untuk kandang ini. - - )} + + {projectName + ? `${projectName} • ${kandangName}` + : kandangName} + + + {[areaName, locationName].filter(Boolean).join(' • ')} + - ); - }) - )} - {/* Footer */} - - - `${pageNumber} / ${totalPages}` - } - fixed - /> - - + {hasData ? ( + <> + {/* Table 1: WOA & BW */} + + 1. WOA & Body Weight + + + + {/* Table 2: Deplesi */} + + 2. Deplesi + + + + {/* Table 3: Butiran */} + + 3. Butiran + + + + {/* Table 4: Berat (Kg) */} + + 4. Berat (Kg) + + + + {/* Table 5: Persentase */} + + 5. Persentase + + + + {/* Table 6: Produksi (HD, FI, EM, EW) */} + + + 6. Produksi (HD, FI, EM, EW) + + + + + {/* Table 7: Produksi (FCR, HH) */} + + 7. Produksi (FCR, HH) + + + + ) : ( + + Tidak ada production result untuk kandang ini. + + )} + + + + ); + }) + )} ); }; From 02d13efc2548bdcd5d3f176906f4504ecc2fe494 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 09:28:24 +0700 Subject: [PATCH 051/149] refactor(FE): Refactor table column headers for clarity and consistency --- .../ProductionResultReportPDF.tsx | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx index 139f4640..6f7b8313 100644 --- a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -55,11 +55,6 @@ const styles = StyleSheet.create({ }, }); -function safeNum(v: unknown): number { - const n = typeof v === 'number' ? v : Number(v); - return Number.isFinite(n) ? n : 0; -} - function valueText(v: unknown) { if (v === null || v === undefined) return '-'; if (typeof v === 'number') return formatNumber(v); @@ -71,9 +66,9 @@ function valueText(v: unknown) { // ======================================== const getBwTableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'woa', header: 'WOA', flex: 0.8, align: 'center' }, - { key: 'bw', header: 'BW', flex: 1, align: 'right' }, - { key: 'std_bw', header: 'Std BW', flex: 1, align: 'right' }, + { key: 'woa', header: 'Week of Age', flex: 0.8, align: 'center' }, + { key: 'bw', header: 'Body Weight', flex: 1, align: 'right' }, + { key: 'std_bw', header: 'Std Body Weight', flex: 1, align: 'right' }, { key: 'uniformity', header: 'Uniformity', flex: 1.2, align: 'right' }, { key: 'std_uniformity', @@ -107,8 +102,13 @@ const getBwTableData = ( // ======================================== const getDepTableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'dep_kum', header: 'Dep Kum', flex: 1.5, align: 'right' }, - { key: 'dep_std', header: 'Dep Std', flex: 1.5, align: 'right' }, + { + key: 'dep_kum', + header: 'Depletion Cummulative', + flex: 1.5, + align: 'right', + }, + { key: 'dep_std', header: 'Depletion Std', flex: 1.5, align: 'right' }, ]; const getDepTableData = ( @@ -210,10 +210,10 @@ const getKgTableData = ( // ======================================== const getPersenTableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'persen_utuh', header: '% Utuh', flex: 1.5, align: 'right' }, - { key: 'persen_putih', header: '% Putih', flex: 1.5, align: 'right' }, - { key: 'persen_retak', header: '% Retak', flex: 1.5, align: 'right' }, - { key: 'persen_pecah', header: '% Pecah', flex: 1.5, align: 'right' }, + { key: 'persen_utuh', header: 'Utuh (%)', flex: 1.5, align: 'right' }, + { key: 'persen_putih', header: 'Putih (%)', flex: 1.5, align: 'right' }, + { key: 'persen_retak', header: '% Retak (%)', flex: 1.5, align: 'right' }, + { key: 'persen_pecah', header: '% Pecah (%)', flex: 1.5, align: 'right' }, ]; const getPersenTableData = ( @@ -251,14 +251,14 @@ const getPersenTableData = ( // ======================================== const getProduksi1TableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'hd', header: 'HD', flex: 0.8, align: 'right' }, - { key: 'hd_std', header: 'HD Std', flex: 1, align: 'right' }, - { key: 'fi', header: 'FI', flex: 0.8, align: 'right' }, - { key: 'fi_std', header: 'FI Std', flex: 1, align: 'right' }, - { key: 'em', header: 'EM', flex: 0.8, align: 'right' }, - { key: 'em_std', header: 'EM Std', flex: 1, align: 'right' }, - { key: 'ew', header: 'EW', flex: 0.8, align: 'right' }, - { key: 'ew_std', header: 'EW Std', flex: 1, align: 'right' }, + { key: 'hd', header: 'Hen Day', flex: 0.8, align: 'right' }, + { key: 'hd_std', header: 'Hen Day Std', flex: 1, align: 'right' }, + { key: 'fi', header: 'Feed Intake', flex: 0.8, align: 'right' }, + { key: 'fi_std', header: 'Feed Intake Std', flex: 1, align: 'right' }, + { key: 'em', header: 'Egg Mass', flex: 0.8, align: 'right' }, + { key: 'em_std', header: 'Egg Mass Std', flex: 1, align: 'right' }, + { key: 'ew', header: 'Egg Weight', flex: 0.8, align: 'right' }, + { key: 'ew_std', header: 'Egg Weight Std', flex: 1, align: 'right' }, ]; const getProduksi1TableData = ( @@ -286,8 +286,8 @@ const getProduksi2TableColumns = (): PdfColumn[] => [ { key: 'no', header: 'No', flex: 0.5, align: 'center' }, { key: 'fcr', header: 'FCR', flex: 1, align: 'right' }, { key: 'fcr_std', header: 'FCR Std', flex: 1.2, align: 'right' }, - { key: 'hh', header: 'HH', flex: 1, align: 'right' }, - { key: 'hh_std', header: 'HH Std', flex: 1.2, align: 'right' }, + { key: 'hh', header: 'Hen House', flex: 1, align: 'right' }, + { key: 'hh_std', header: 'Hen House Std', flex: 1.2, align: 'right' }, ]; const getProduksi2TableData = ( From 70b63f7773e58141727929103f8bcad87a5b1905 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 10:38:51 +0700 Subject: [PATCH 052/149] refactor(FE): Refactor PdfTable components to support generic data types --- src/components/helper/pdf/table/PdfTable.tsx | 35 ++++--- src/components/helper/pdf/table/PdfTbody.tsx | 98 +++++++++----------- src/components/helper/pdf/table/PdfTfoot.tsx | 86 +++++++++-------- src/components/helper/pdf/table/PdfThead.tsx | 43 ++++++--- src/components/helper/pdf/table/index.ts | 4 +- src/components/helper/pdf/table/types.ts | 24 +++++ 6 files changed, 167 insertions(+), 123 deletions(-) create mode 100644 src/components/helper/pdf/table/types.ts diff --git a/src/components/helper/pdf/table/PdfTable.tsx b/src/components/helper/pdf/table/PdfTable.tsx index 27369db5..86f4ed77 100644 --- a/src/components/helper/pdf/table/PdfTable.tsx +++ b/src/components/helper/pdf/table/PdfTable.tsx @@ -1,9 +1,10 @@ 'use client'; import { View, StyleSheet } from '@react-pdf/renderer'; -import { PdfThead, PdfColumn } from './PdfThead'; -import { PdfTbody, PdfTbodyCell } from './PdfTbody'; -import { PdfTfoot, PdfTfootCell } from './PdfTfoot'; +import type { PdfColumn } from './types'; +import { PdfThead } from './PdfThead'; +import { PdfTbody } from './PdfTbody'; +import { PdfTfoot } from './PdfTfoot'; const styles = StyleSheet.create({ table: { @@ -13,10 +14,10 @@ const styles = StyleSheet.create({ }, }); -interface PdfTableProps { - columns: PdfColumn[]; - data: PdfTbodyCell[][]; - footer?: PdfTfootCell[]; +interface PdfTableProps> { + columns: PdfColumn[]; + data: TData[]; + showFooter?: boolean; footerLabel?: string; firstRow?: { valueKey: string; @@ -26,20 +27,26 @@ interface PdfTableProps { }; } -export const PdfTable = ({ +export const PdfTable = ,>({ columns, data, - footer, + showFooter = false, footerLabel = 'Total', firstRow, -}: PdfTableProps) => { +}: PdfTableProps) => { + // Check if any column has footer defined + const hasFooter = + showFooter || columns.some((col) => col.footer !== undefined); + return ( - - - {footer && footer.length > 0 && ( - + + + {hasFooter && data.length > 0 && ( + )} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfTbody.tsx b/src/components/helper/pdf/table/PdfTbody.tsx index fee79726..cc9fe41d 100644 --- a/src/components/helper/pdf/table/PdfTbody.tsx +++ b/src/components/helper/pdf/table/PdfTbody.tsx @@ -1,22 +1,8 @@ 'use client'; import { Text, View, StyleSheet } from '@react-pdf/renderer'; - -export interface PdfColumn { - key: string; - header: string; - flex: number; - align?: 'left' | 'center' | 'right'; -} - -export interface PdfTbodyCell { - key: string; - value: string | number | React.ReactNode; - align?: 'left' | 'center' | 'right'; - color?: string; - formatAs?: 'text' | 'date' | 'currency' | 'number'; - formatDate?: string; -} +import { ReactNode } from 'react'; +import type { PdfColumn } from './types'; const styles = StyleSheet.create({ tableRow: { @@ -71,21 +57,22 @@ const styles = StyleSheet.create({ }, }); -interface PdfTbodyProps { - columns: PdfColumn[]; - rows: PdfTbodyCell[][]; +interface PdfTbodyProps> { + columns: PdfColumn[]; + data: TData[]; firstRow?: { valueKey: string; value: number; align?: 'right'; color?: string; }; - formatDate?: (date: string, format: string) => string; - formatNumber?: (num: number) => string; - formatCurrency?: (num: number) => string; } -export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { +export const PdfTbody = ,>({ + columns, + data, + firstRow, +}: PdfTbodyProps) => { return ( <> {/* First Row */} @@ -93,17 +80,17 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { {columns.map((column, index) => { const isLastColumn = index === columns.length - 1; - const isfirstRowColumn = column.key === firstRow.valueKey; - const align = column.align || 'center'; + const isFirstRowColumn = column.key === firstRow.valueKey; + const align = column.align || 'left'; const cellStyle = column.key === 'no' - ? [styles.tableCellNo, { flex: column.flex }] - : isfirstRowColumn + ? [styles.tableCellNo, { flex: column.flex || 1 }] + : isFirstRowColumn ? [ styles.tableCellRight, { - flex: column.flex, + flex: column.flex || 1, color: firstRow.color || 'black', borderRightWidth: isLastColumn ? 0 : 1, }, @@ -112,7 +99,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellRight, { - flex: column.flex, + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] @@ -120,7 +107,7 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellCenter, { - flex: column.flex, + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] @@ -128,15 +115,15 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellLast, { - flex: column.flex, + flex: column.flex || 1, borderRightWidth: 0, }, ] - : [styles.tableCell, { flex: column.flex }]; + : [styles.tableCell, { flex: column.flex || 1 }]; return ( - {isfirstRowColumn ? firstRow.value : ''} + {isFirstRowColumn ? firstRow.value : ''} ); })} @@ -144,8 +131,8 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { )} {/* Data Rows */} - {rows.map((row, rowIndex) => { - const isLastRow = rowIndex === rows.length - 1; + {data.map((row, rowIndex) => { + const isLastRow = rowIndex === data.length - 1; return ( { ]} > {columns.map((column, colIndex) => { - const cell = row.find((c) => c.key === column.key); const isLastColumn = colIndex === columns.length - 1; - const align = cell?.align || column.align || 'center'; + const align = column.align || 'left'; + + // Get cell content from column.cell function or fallback to row value + let cellContent: ReactNode; + if (column.cell) { + cellContent = column.cell({ row, index: rowIndex }); + } else { + cellContent = + ((row as Record)[column.key] as ReactNode) ?? + '-'; + } const cellStyle = column.key === 'no' - ? [styles.tableCellNo, { flex: column.flex }] + ? [styles.tableCellNo, { flex: column.flex || 1 }] : align === 'right' ? [ styles.tableCellRight, { - flex: column.flex, - color: cell?.color || 'black', + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] @@ -176,37 +171,30 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ? [ styles.tableCellCenter, { - flex: column.flex, - color: cell?.color || 'black', + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ] : isLastColumn ? [ styles.tableCellLast, - { flex: column.flex, borderRightWidth: 0 }, + { flex: column.flex || 1, borderRightWidth: 0 }, ] : [ styles.tableCell, { - flex: column.flex, - color: cell?.color || 'black', + flex: column.flex || 1, borderRightWidth: isLastColumn ? 0 : 1, }, ]; return ( - {cell?.value !== undefined && - cell?.value !== null && - cell?.value !== '' ? ( - typeof cell.value === 'object' ? ( - cell.value - ) : ( - {String(cell.value)} - ) + {typeof cellContent === 'string' || + typeof cellContent === 'number' ? ( + {String(cellContent)} ) : ( - - + cellContent )} ); @@ -217,3 +205,5 @@ export const PdfTbody = ({ columns, rows, firstRow }: PdfTbodyProps) => { ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfTfoot.tsx b/src/components/helper/pdf/table/PdfTfoot.tsx index a9f209b1..9d974f38 100644 --- a/src/components/helper/pdf/table/PdfTfoot.tsx +++ b/src/components/helper/pdf/table/PdfTfoot.tsx @@ -1,21 +1,8 @@ 'use client'; import { Text, View, StyleSheet } from '@react-pdf/renderer'; - -export interface PdfColumn { - key: string; - header: string; - flex: number; - align?: 'left' | 'center' | 'right'; -} - -export interface PdfTfootCell { - key: string; - value: string | number; - align?: 'left' | 'center' | 'right'; - flex?: number; - color?: string; -} +import { ReactNode } from 'react'; +import type { PdfColumn } from './types'; const styles = StyleSheet.create({ tableRow: { @@ -69,63 +56,86 @@ const styles = StyleSheet.create({ }, }); -interface PdfTfootProps { - columns: PdfColumn[]; - cells: PdfTfootCell[]; +interface PdfTfootProps> { + columns: PdfColumn[]; + data: TData[]; label?: string; } -export const PdfTfoot = ({ +export const PdfTfoot = ,>({ columns, - cells, + data, label = 'Total', -}: PdfTfootProps) => { +}: PdfTfootProps) => { return ( {columns.map((column, index) => { const isLastColumn = index === columns.length - 1; - const cellData = cells.find((c) => c.key === column.key); + + // Get footer content from column definition + let footerContent: ReactNode; + if (typeof column.footer === 'function') { + footerContent = column.footer(data); + } else { + footerContent = column.footer; + } + + // Use label for first column (usually 'no' column) + const displayContent = column.key === 'no' ? label : footerContent; + + // Determine alignment + const align = column.footerAlign || column.align || 'left'; + const color = column.footerColor || 'black'; const cellStyle = column.key === 'no' ? [ styles.tableCellNo, - { flex: column.flex, borderRightWidth: isLastColumn ? 0 : 1 }, + { + flex: column.flex || 1, + borderRightWidth: isLastColumn ? 0 : 1, + color, + }, ] - : cellData?.align === 'right' + : align === 'right' ? [ styles.tableCellRight, { - flex: column.flex, - color: cellData?.color || 'black', + flex: column.flex || 1, + color, borderRightWidth: isLastColumn ? 0 : 1, }, ] - : cellData?.align === 'center' + : align === 'center' ? [ styles.tableCellCenter, { - flex: column.flex, - color: cellData?.color || 'black', + flex: column.flex || 1, + color, borderRightWidth: isLastColumn ? 0 : 1, }, ] : isLastColumn - ? [styles.tableCellLast, { flex: column.flex }] - : [ - styles.tableCell, - { - flex: column.flex, - color: cellData?.color || 'black', - }, - ]; + ? [styles.tableCellLast, { flex: column.flex || 1, color }] + : [styles.tableCell, { flex: column.flex || 1, color }]; return ( - {column.key === 'no' ? label : cellData?.value || ''} + {displayContent !== undefined && displayContent !== null ? ( + typeof displayContent === 'string' || + typeof displayContent === 'number' ? ( + {String(displayContent)} + ) : ( + displayContent + ) + ) : ( + - + )} ); })} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/PdfThead.tsx b/src/components/helper/pdf/table/PdfThead.tsx index 89037216..889f9f34 100644 --- a/src/components/helper/pdf/table/PdfThead.tsx +++ b/src/components/helper/pdf/table/PdfThead.tsx @@ -1,13 +1,8 @@ 'use client'; import { Text, View, StyleSheet } from '@react-pdf/renderer'; - -export interface PdfColumn { - key: string; - header: string; - flex: number; - align?: 'left' | 'center' | 'right'; -} +import { ReactNode } from 'react'; +import type { PdfColumn } from './types'; const styles = StyleSheet.create({ tableRow: { @@ -48,23 +43,37 @@ const styles = StyleSheet.create({ }, }); -interface PdfTheadProps { - columns: PdfColumn[]; +interface PdfTheadProps> { + columns: PdfColumn[]; + data?: TData[]; } -export const PdfThead = ({ columns }: PdfTheadProps) => { +export const PdfThead = ,>({ + columns, + data, +}: PdfTheadProps) => { return ( {columns.map((column, index) => { - const align = column.align || 'center'; const isLastColumn = index === columns.length - 1; + // Get header content from column definition + let headerContent: ReactNode; + if (typeof column.header === 'function') { + headerContent = column.header(data || []); + } else { + headerContent = column.header || column.key; + } + + // Determine alignment - columns align right by default for numeric data + const align = column.align || 'left'; + const cellStyle = align === 'right' ? [ styles.tableCellHeaderRight, { - flex: column.flex, + flex: column.flex || 1, textAlign: 'right' as const, borderRightWidth: isLastColumn ? 0 : 1, }, @@ -72,7 +81,7 @@ export const PdfThead = ({ columns }: PdfTheadProps) => { : [ styles.tableCellHeader, { - flex: column.flex, + flex: column.flex || 1, textAlign: align as 'left' | 'center' | 'right', borderRightWidth: isLastColumn ? 0 : 1, }, @@ -80,10 +89,16 @@ export const PdfThead = ({ columns }: PdfTheadProps) => { return ( - {column.header} + {typeof headerContent === 'string' ? ( + {headerContent} + ) : ( + headerContent + )} ); })} ); }; + +export type { PdfColumn }; diff --git a/src/components/helper/pdf/table/index.ts b/src/components/helper/pdf/table/index.ts index 35839f17..3c780688 100644 --- a/src/components/helper/pdf/table/index.ts +++ b/src/components/helper/pdf/table/index.ts @@ -2,6 +2,4 @@ export { PdfTable } from './PdfTable'; export { PdfThead } from './PdfThead'; export { PdfTbody } from './PdfTbody'; export { PdfTfoot } from './PdfTfoot'; -export type { PdfColumn } from './PdfThead'; -export type { PdfTbodyCell } from './PdfTbody'; -export type { PdfTfootCell } from './PdfTfoot'; +export type { PdfColumn } from './types'; diff --git a/src/components/helper/pdf/table/types.ts b/src/components/helper/pdf/table/types.ts new file mode 100644 index 00000000..c2437f13 --- /dev/null +++ b/src/components/helper/pdf/table/types.ts @@ -0,0 +1,24 @@ +import { ReactNode } from 'react'; + +/** + * PdfColumn - Mirip dengan ColumnDef di TanStack Table + * Mengatur header (thead), body (tbody), dan footer (tfoot) dalam satu definisi + */ +export interface PdfColumn> { + key: string; + flex?: number; + + // Header configuration (thead) + header?: string | ((data: TData[]) => ReactNode); + + // Body configuration (tbody) + align?: 'left' | 'center' | 'right'; + cell?: (props: { row: TData; index: number }) => ReactNode | string | number; + + // Footer configuration (tfoot) + footer?: string | number | ((data: TData[]) => ReactNode | string | number); + footerAlign?: 'left' | 'center' | 'right'; + footerColor?: string; +} + +export type { PdfColumn as default }; From 0f1d2ce4773d6dd3f4ed7faccedbaf75abc32d87 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 10:39:37 +0700 Subject: [PATCH 053/149] refactor(FE): Refactor PDF table components to simplify imports --- .../export/CustomerPaymentExportPDF.tsx | 295 +++++----- .../finance/export/DebtSupllierExportPDF.tsx | 285 ++++++---- .../export/PurchasesPerSupplierExportPDF.tsx | 209 ++++---- .../export/DailyMarketingReportPDF.tsx | 284 +++++----- .../export/HppPerkandangExportPDF.tsx | 280 ++++------ .../ProductionResultReportPDF.tsx | 505 ++++++++++-------- 6 files changed, 973 insertions(+), 885 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index b0cb5c8d..4a6f0238 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -7,6 +7,7 @@ import { StyleSheet, Font, pdf, + Text, } from '@react-pdf/renderer'; import { @@ -16,12 +17,7 @@ import { formatTitleCase, } from '@/lib/helper'; import { CustomerPaymentReport } from '@/types/api/report/customer-payment'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, - PdfTfootCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; @@ -61,172 +57,183 @@ interface CustomerPaymentExportPDFParams { }; } -const getTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'trans_date', header: 'Tanggal DO', flex: 1.2, align: 'center' }, +const getTableColumns = ( + summary?: CustomerPaymentReport['summary'] +): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + footer: 'Total', + }, + { + key: 'trans_date', + header: 'Tanggal DO', + flex: 1.2, + align: 'center', + cell: ({ row }) => + row.trans_date ? formatDate(row.trans_date, 'DD MMM YY') : '-', + footer: '', + }, { key: 'delivery_date', header: 'Tanggal Realisasi', flex: 1.2, align: 'center', + cell: ({ row }) => + row.delivery_date ? formatDate(row.delivery_date, 'DD MMM YY') : '-', + footer: '', + }, + { + key: 'aging', + header: 'Aging', + flex: 0.8, + align: 'center', + cell: ({ row }) => + row.aging_day != null ? `${formatNumber(row.aging_day)} hari` : '-', + footer: '', + }, + { + key: 'reference', + header: 'Referensi', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.reference || '-', + footer: '', + }, + { + key: 'vehicle_numbers', + header: 'No Polisi', + flex: 1.2, + align: 'left', + cell: ({ row }) => + Array.isArray(row.vehicle_numbers) && row.vehicle_numbers.length > 0 + ? row.vehicle_numbers.join(', ') + : '-', + footer: '', + }, + { + key: 'qty', + header: 'Qty', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.qty), + footer: summary ? formatNumber(summary.total_qty || 0) : '', + footerAlign: 'right', + }, + { + key: 'weight', + header: 'Berat', + flex: 1, + align: 'right', + cell: ({ row }) => formatNumber(row.weight), + footer: summary ? formatNumber(summary.total_weight || 0) : '', + footerAlign: 'right', + }, + { + key: 'average_weight', + header: 'Rata-Rata', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.average_weight), + footer: '', + }, + { + key: 'unit_price', + header: 'Harga/Unit (Rp)', + flex: 1.2, + align: 'right', + cell: ({ row }) => formatCurrency(row.unit_price), + footer: '', + }, + { + key: 'final_price', + header: 'Harga Akhir (Rp)', + flex: 1.2, + align: 'right', + cell: ({ row }) => formatCurrency(row.final_price), + footer: summary ? formatCurrency(summary.total_final_amount || 0) : '', + footerAlign: 'right', + }, + { + key: 'total_price', + header: 'Total (Rp)', + flex: 1.2, + align: 'right', + cell: ({ row }) => formatCurrency(row.total_price), + footer: summary ? formatCurrency(summary.total_grand_amount || 0) : '', + footerAlign: 'right', }, - { key: 'aging', header: 'Aging', flex: 0.8, align: 'center' }, - { key: 'reference', header: 'Referensi', flex: 1.5, align: 'left' }, - { key: 'vehicle_numbers', header: 'No Polisi', flex: 1.2, align: 'left' }, - { key: 'qty', header: 'Qty', flex: 0.8, align: 'right' }, - { key: 'weight', header: 'Berat', flex: 1, align: 'right' }, - { key: 'average_weight', header: 'Rata-Rata', flex: 0.8, align: 'right' }, - { key: 'unit_price', header: 'Harga/Unit (Rp)', flex: 1.2, align: 'right' }, - { key: 'final_price', header: 'Harga Akhir (Rp)', flex: 1.2, align: 'right' }, - { key: 'total_price', header: 'Total (Rp)', flex: 1.2, align: 'right' }, { key: 'payment_amount', header: 'Pembayaran (Rp)', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.payment_amount), + footer: summary ? formatCurrency(summary.total_payment || 0) : '', + footerAlign: 'right', }, { key: 'accounts_receivable', header: 'Saldo (Rp)', flex: 1.2, align: 'right', + cell: ({ row }) => ( + + {formatCurrency(row.accounts_receivable)} + + ), + footer: summary + ? formatCurrency(summary.total_accounts_receivable || 0) + : '', + footerAlign: 'right', + footerColor: + (summary?.total_accounts_receivable || 0) < 0 ? 'red' : undefined, }, - { key: 'status', header: 'Keterangan', flex: 1.5, align: 'center' }, - { key: 'pickup_info', header: 'Pengambilan', flex: 1, align: 'left' }, - { key: 'sales_person', header: 'Sales', flex: 1.5, align: 'left' }, -]; - -const getTableData = ( - rows: CustomerPaymentReport['rows'] -): PdfTbodyCell[][] => { - return rows.map((item, index) => [ - { key: 'no', value: index + 1 }, - { - key: 'trans_date', - value: item.trans_date ? formatDate(item.trans_date, 'DD MMM YY') : '-', - }, - { - key: 'delivery_date', - value: item.delivery_date - ? formatDate(item.delivery_date, 'DD MMM YY') - : '-', - }, - { - key: 'aging', - value: - item.aging_day != null ? `${formatNumber(item.aging_day)} hari` : '-', - }, - { key: 'reference', value: item.reference || '-' }, - { - key: 'vehicle_numbers', - value: - Array.isArray(item.vehicle_numbers) && item.vehicle_numbers.length > 0 - ? item.vehicle_numbers.join(', ') - : '-', - }, - { key: 'qty', value: formatNumber(item.qty), align: 'right' }, - { key: 'weight', value: formatNumber(item.weight), align: 'right' }, - { - key: 'average_weight', - value: formatNumber(item.average_weight), - align: 'right', - }, - { - key: 'unit_price', - value: formatCurrency(item.unit_price), - align: 'right', - }, - { - key: 'final_price', - value: formatCurrency(item.final_price), - align: 'right', - }, - { - key: 'total_price', - value: formatCurrency(item.total_price), - align: 'right', - }, - { - key: 'payment_amount', - value: formatCurrency(item.payment_amount), - align: 'right', - }, - { - key: 'accounts_receivable', - value: formatCurrency(item.accounts_receivable), - align: 'right', - color: item.accounts_receivable < 0 ? 'red' : undefined, - }, - { - key: 'status', - value: item.status ? ( + { + key: 'status', + header: 'Keterangan', + flex: 1.5, + align: 'center', + cell: ({ row }) => + row.status ? ( - {formatTitleCase(item.status)} + {formatTitleCase(row.status)} ) : ( '-' ), - }, - { - key: 'pickup_info', - value: - Array.isArray(item.pickup_info) && item.pickup_info.length > 0 - ? item.pickup_info.join(', ') - : '-', - }, - { key: 'sales_person', value: item.sales_person || '-' }, - ]); -}; - -const getTableFooter = ( - summary: CustomerPaymentReport['summary'] -): PdfTfootCell[] => [ - { key: 'no', value: 'Total' }, - { key: 'trans_date', value: '' }, - { key: 'delivery_date', value: '' }, - { key: 'aging', value: '' }, - { key: 'reference', value: '' }, - { key: 'vehicle_numbers', value: '' }, - { key: 'qty', value: formatNumber(summary?.total_qty || 0), align: 'right' }, - { - key: 'weight', - value: formatNumber(summary?.total_weight || 0), - align: 'right', - }, - { key: 'average_weight', value: '' }, - { key: 'unit_price', value: '' }, - { - key: 'final_price', - value: formatCurrency(summary?.total_final_amount || 0), - align: 'right', + footer: '', }, { - key: 'total_price', - value: formatCurrency(summary?.total_grand_amount || 0), - align: 'right', + key: 'pickup_info', + header: 'Pengambilan', + flex: 1, + align: 'left', + cell: ({ row }) => + Array.isArray(row.pickup_info) && row.pickup_info.length > 0 + ? row.pickup_info.join(', ') + : '-', + footer: '', }, { - key: 'payment_amount', - value: formatCurrency(summary?.total_payment || 0), - align: 'right', + key: 'sales_person', + header: 'Sales', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.sales_person || '-', + footer: '', }, - { - key: 'accounts_receivable', - value: formatCurrency(summary?.total_accounts_receivable || 0), - align: 'right', - color: (summary?.total_accounts_receivable || 0) < 0 ? 'red' : undefined, - }, - { key: 'status', value: '' }, - { key: 'pickup_info', value: '' }, - { key: 'sales_person', value: '' }, ]; const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { @@ -276,13 +283,9 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {/* Table */} [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'pr_number', header: 'No. PR', flex: 1, align: 'left' }, - { key: 'po_number', header: 'No. PO', flex: 1, align: 'left' }, - { - key: 'received_date', - header: 'Tgl Terima/Bayar', - flex: 0.7, - align: 'center', - }, - { key: 'po_date', header: 'Tgl PO', flex: 0.7, align: 'center' }, - { key: 'aging', header: 'Aging', flex: 0.6, align: 'center' }, - { key: 'area', header: 'Area', flex: 1, align: 'left' }, - { key: 'warehouse', header: 'Gudang', flex: 1, align: 'left' }, - { key: 'due_date', header: 'Jatuh Tempo', flex: 1, align: 'center' }, - { key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'center' }, - { - key: 'total_price', - header: 'Nominal Pembelian (Rp)', - flex: 1.5, - align: 'right', - }, - { - key: 'payment_price', - header: 'Pembayaran (Rp)', - flex: 1.5, - align: 'right', - }, - { - key: 'balance', - header: 'Sisa Saldo Hutang (Rp)', - flex: 1.5, - align: 'right', - }, - { key: 'status', header: 'Status', flex: 1.2, align: 'center' }, - { key: 'travel_number', header: 'No. Perjalanan', flex: 1, align: 'left' }, -]; +const getTableColumns = (total?: DebtSupplier['total']): PdfColumn[] => { + type DebtRow = DebtSupplier['rows'][number]; -const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => { - return rows.map((item, index) => [ - { key: 'no', value: index + 1 }, - { key: 'pr_number', value: item.pr_number || '-' }, - { key: 'po_number', value: item.po_number || '-' }, + return [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + footer: 'Total', + }, + { + key: 'pr_number', + header: 'No. PR', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).pr_number || '-', + footer: '', + }, + { + key: 'po_number', + header: 'No. PO', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).po_number || '-', + footer: '', + }, { key: 'received_date', - value: item.received_date - ? formatDate(item.received_date, 'DD MMM YY') - : '-', + header: 'Tgl Terima/Bayar', + flex: 0.7, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).received_date + ? formatDate((row as unknown as DebtRow).received_date, 'DD MMM YY') + : '-', + footer: '', }, { key: 'po_date', - value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-', + header: 'Tgl PO', + flex: 0.7, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).po_date + ? formatDate((row as unknown as DebtRow).po_date, 'DD MMM YY') + : '-', + footer: '', }, { key: 'aging', - value: item.aging != null ? `${formatNumber(item.aging)}` : '-', + header: 'Aging', + flex: 0.6, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).aging != null + ? `${formatNumber((row as unknown as DebtRow).aging)}` + : '-', + footer: total ? formatNumber(total.aging || 0) + ' Hari' : '', + }, + { + key: 'area', + header: 'Area', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).area?.name || '-', + footer: '', + }, + { + key: 'warehouse', + header: 'Gudang', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).warehouse?.name || '-', + footer: '', }, - { key: 'area', value: item.area?.name || '-' }, - { key: 'warehouse', value: item.warehouse?.name || '-' }, { key: 'due_date', - value: item.due_date ? formatDate(item.due_date, 'DD MMM YY') : '-', + header: 'Jatuh Tempo', + flex: 1, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).due_date + ? formatDate((row as unknown as DebtRow).due_date, 'DD MMM YY') + : '-', + footer: '', }, { key: 'due_status', - value: - item.due_status && item.due_status !== '-' ? ( + header: 'Status Jatuh Tempo', + flex: 2, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).due_status && + (row as unknown as DebtRow).due_status !== '-' ? ( - {item.due_status} + {(row as unknown as DebtRow).due_status} ) : ( '-' ), + footer: '', }, { key: 'total_price', - value: formatCurrency(item.total_price), + header: 'Nominal Pembelian (Rp)', + flex: 1.5, align: 'right', - color: item.total_price < 0 ? 'red' : undefined, + cell: ({ row }) => ( + + {formatCurrency((row as unknown as DebtRow).total_price)} + + ), + footer: total ? formatCurrency(total.total_price || 0) : '', + footerAlign: 'right', }, { key: 'payment_price', - value: formatCurrency(item.payment_price), + header: 'Pembayaran (Rp)', + flex: 1.5, align: 'right', - color: item.payment_price < 0 ? 'red' : undefined, + cell: ({ row }) => ( + + {formatCurrency((row as unknown as DebtRow).payment_price)} + + ), + footer: total ? formatCurrency(total.payment_price || 0) : '', + footerAlign: 'right', }, { key: 'balance', - value: formatCurrency(item.balance), + header: 'Sisa Saldo Hutang (Rp)', + flex: 1.5, align: 'right', - color: item.balance < 0 ? 'red' : undefined, + cell: ({ row }) => ( + + {formatCurrency((row as unknown as DebtRow).balance)} + + ), + footer: total ? formatCurrency(total.debt_price || 0) : '', + footerAlign: 'right', + footerColor: (total?.debt_price || 0) < 0 ? 'red' : undefined, }, { key: 'status', - value: - item.status && item.status !== '-' ? ( + header: 'Status', + flex: 1.2, + align: 'center', + cell: ({ row }) => + (row as unknown as DebtRow).status && + (row as unknown as DebtRow).status !== '-' ? ( - {item.status} + {(row as unknown as DebtRow).status} ) : ( '-' ), + footer: '', }, - { key: 'travel_number', value: item.travel_number || '-' }, - ]); + { + key: 'travel_number', + header: 'No. Perjalanan', + flex: 1, + align: 'left', + cell: ({ row }) => (row as unknown as DebtRow).travel_number || '-', + footer: '', + }, + ]; }; -const getTableFooter = (total: DebtSupplier['total']): PdfTfootCell[] => [ - { key: 'no', value: 'Total' }, - { key: 'pr_number', value: '' }, - { key: 'po_number', value: '' }, - { key: 'received_date', value: '' }, - { key: 'po_date', value: '' }, - { key: 'aging', value: formatNumber(total?.aging || 0) + ' Hari' }, - { key: 'area', value: '' }, - { key: 'warehouse', value: '' }, - { key: 'due_date', value: '' }, - { key: 'due_status', value: '' }, - { - key: 'total_price', - value: formatCurrency(total?.total_price || 0), - align: 'right', - }, - { - key: 'payment_price', - value: formatCurrency(total?.payment_price || 0), - align: 'right', - }, - { - key: 'balance', - value: formatCurrency(total?.debt_price || 0), - align: 'right', - color: (total?.debt_price || 0) < 0 ? 'red' : undefined, - }, - { key: 'status', value: '' }, - { key: 'travel_number', value: '' }, -]; - interface DebtSupplierExportPDFParams { data: DebtSupplier[]; params?: { @@ -263,13 +324,9 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {/* Table */} []} + showFooter={!!supplierReport.total} firstRow={ typeof supplierReport.initial_balance === 'number' && supplierReport.initial_balance !== 0 diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx index 3423ca69..d265d08f 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx @@ -10,12 +10,7 @@ import { } from '@react-pdf/renderer'; import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, - PdfTfootCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; @@ -58,90 +53,118 @@ interface PurchasesPerSupplierExportParams { }; } -const getTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, +const getTableColumns = ( + summary?: LogisticPurchasePerSupplierReport['summary'] +): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + footer: 'Total', + }, { key: 'receive_date', header: 'Tanggal Terima', flex: 1.2, align: 'center', + cell: ({ row }) => + row.receive_date ? formatDate(row.receive_date, 'DD MMM YY') : '-', + footer: '', + }, + { + key: 'po_date', + header: 'Tanggal PO', + flex: 1.2, + align: 'center', + cell: ({ row }) => + row.po_date ? formatDate(row.po_date, 'DD MMM YY') : '-', + footer: '', + }, + { + key: 'po_number', + header: 'No. Referensi', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.po_number || '-', + footer: '', + }, + { + key: 'product', + header: 'Nama Produk', + flex: 2, + align: 'left', + cell: ({ row }) => row.product?.name || '-', + footer: '', + }, + { + key: 'warehouse', + header: 'Tujuan', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.warehouse?.name || '-', + footer: '', + }, + { + key: 'qty', + header: 'QTY', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.qty || 0), + footer: summary ? formatNumber(summary.total_qty || 0) : '', + footerAlign: 'right', + }, + { + key: 'unit_price', + header: 'Harga Beli (Rp)', + flex: 1.5, + align: 'right', + cell: ({ row }) => formatCurrency(row.unit_price || 0), + footer: '', }, - { key: 'po_date', header: 'Tanggal PO', flex: 1.2, align: 'center' }, - { key: 'po_number', header: 'No. Referensi', flex: 1.5, align: 'left' }, - { key: 'product', header: 'Nama Produk', flex: 2, align: 'left' }, - { key: 'warehouse', header: 'Tujuan', flex: 1.5, align: 'left' }, - { key: 'qty', header: 'QTY', flex: 0.8, align: 'right' }, - { key: 'unit_price', header: 'Harga Beli (Rp)', flex: 1.5, align: 'right' }, { key: 'purchase_value', header: 'Value Harga Beli (Rp)', flex: 1.8, align: 'right', + cell: ({ row }) => formatCurrency(row.purchase_value || 0), + footer: summary ? formatCurrency(summary.total_purchase_value || 0) : '', + footerAlign: 'right', }, { key: 'transport_unit_price', header: 'Transport (Rp)', flex: 1.3, align: 'right', + cell: ({ row }) => formatCurrency(row.transport_unit_price || 0), + footer: '', }, { key: 'transport_value', header: 'Value Transport (Rp)', flex: 1.8, align: 'right', + cell: ({ row }) => formatCurrency(row.transport_value || 0), + footer: summary ? formatCurrency(summary.total_transport_value || 0) : '', + footerAlign: 'right', }, - { key: 'total_amount', header: 'Jumlah (Rp)', flex: 1.5, align: 'right' }, - { key: 'expedition', header: 'Ekspedisi', flex: 1.2, align: 'center' }, - { key: 'delivery_number', header: 'Surat Jalan', flex: 1.2, align: 'left' }, -]; - -const getTableData = ( - rows: LogisticPurchasePerSupplierReport['rows'] -): PdfTbodyCell[][] => { - return rows.map((item, index) => [ - { key: 'no', value: index + 1 }, - { - key: 'receive_date', - value: item.receive_date - ? formatDate(item.receive_date, 'DD MMM YY') - : '-', - }, - { - key: 'po_date', - value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-', - }, - { key: 'po_number', value: item.po_number || '-' }, - { key: 'product', value: item.product?.name || '-' }, - { key: 'warehouse', value: item.warehouse?.name || '-' }, - { key: 'qty', value: formatNumber(item.qty || 0), align: 'right' }, - { - key: 'unit_price', - value: formatCurrency(item.unit_price || 0), - align: 'right', - }, - { - key: 'purchase_value', - value: formatCurrency(item.purchase_value || 0), - align: 'right', - }, - { - key: 'transport_unit_price', - value: formatCurrency(item.transport_unit_price || 0), - align: 'right', - }, - { - key: 'transport_value', - value: formatCurrency(item.transport_value || 0), - align: 'right', - }, - { - key: 'total_amount', - value: formatCurrency(item.total_amount || 0), - align: 'right', - }, - { - key: 'expedition', - value: item.expedition ? ( + { + key: 'total_amount', + header: 'Jumlah (Rp)', + flex: 1.5, + align: 'right', + cell: ({ row }) => formatCurrency(row.total_amount || 0), + footer: summary ? formatCurrency(summary.total_amount || 0) : '', + footerAlign: 'right', + }, + { + key: 'expedition', + header: 'Ekspedisi', + flex: 1.2, + align: 'center', + cell: ({ row }) => + row.expedition ? ( - {item.expedition} + {row.expedition} ) : ( '-' ), - }, - { key: 'delivery_number', value: item.delivery_number || '-' }, - ]); -}; - -const getTableFooter = ( - summary: LogisticPurchasePerSupplierReport['summary'] -): PdfTfootCell[] => [ - { key: 'no', value: 'Total' }, - { key: 'receive_date', value: '' }, - { key: 'po_date', value: '' }, - { key: 'po_number', value: '' }, - { key: 'product', value: '' }, - { key: 'warehouse', value: '' }, - { - key: 'qty', - value: formatNumber(summary?.total_qty || 0), - align: 'right', - }, - { key: 'unit_price', value: '' }, - { - key: 'purchase_value', - value: formatCurrency(summary?.total_purchase_value || 0), - align: 'right', - }, - { key: 'transport_unit_price', value: '' }, - { - key: 'transport_value', - value: formatCurrency(summary?.total_transport_value || 0), - align: 'right', + footer: '', }, { - key: 'total_amount', - value: formatCurrency(summary?.total_amount || 0), - align: 'right', + key: 'delivery_number', + header: 'Surat Jalan', + flex: 1.2, + align: 'left', + cell: ({ row }) => row.delivery_number || '-', + footer: '', }, - { key: 'expedition', value: '' }, - { key: 'delivery_number', value: '' }, ]; const createPDFDocument = (params: PurchasesPerSupplierExportParams) => { @@ -253,13 +248,9 @@ const createPDFDocument = (params: PurchasesPerSupplierExportParams) => { {/* Table */}
))} diff --git a/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx index 53b36b80..29c1d619 100644 --- a/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx +++ b/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx @@ -11,12 +11,7 @@ import { formatNumber, formatTitleCase, } from '@/lib/helper'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, - PdfTfootCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; @@ -49,60 +44,90 @@ interface DailyMarketingReportPDFProps { total?: SalesSummary; } -const getTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'so_date', header: 'Tanggal Sales Order', flex: 1.3, align: 'center' }, +const getTableColumns = ( + summary?: SalesSummary +): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'so_date', + header: 'Tanggal Sales Order', + flex: 1.3, + align: 'center', + cell: ({ row }) => + row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-', + }, { key: 'do_date', header: 'Tanggal Delivery Order', flex: 1.3, align: 'center', - }, - { key: 'aging', header: 'Aging (Hari)', flex: 0.7, align: 'center' }, - { key: 'warehouse', header: 'Gudang', flex: 1.2, align: 'left' }, - { key: 'customer', header: 'Pelanggan', flex: 1.5, align: 'left' }, - { key: 'sales', header: 'Sales', flex: 1, align: 'left' }, - { key: 'product', header: 'Produk', flex: 1.3, align: 'left' }, - { key: 'do_number', header: 'Nomor DO', flex: 1.2, align: 'left' }, - { key: 'vehicle', header: 'Nomor Polisi', flex: 1, align: 'left' }, - { key: 'marketing_type', header: 'Tipe Marketing', flex: 1, align: 'center' }, - { key: 'qty', header: 'Quantity', flex: 0.7, align: 'right' }, - { key: 'avg_weight', header: 'Rata-Rata (Kg)', flex: 0.8, align: 'right' }, - { - key: 'total_weight', - header: 'Total Berat (Kg)', - flex: 0.9, - align: 'right', - }, - { key: 'sales_price', header: 'Harga Jual (Rp)', flex: 0.9, align: 'right' }, - { key: 'hpp_price', header: 'HPP (Rp)', flex: 1.3, align: 'right' }, - { key: 'sales_amount', header: 'Total Jual (Rp)', flex: 1, align: 'right' }, - { key: 'hpp_amount', header: 'Total HPP (Rp)', flex: 1.3, align: 'right' }, -]; - -const getTableData = (rows: DailyMarketingReport): PdfTbodyCell[][] => { - return rows.map((row, index) => [ - { key: 'no', value: index + 1 }, - { - key: 'so_date', - value: row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-', - }, - { - key: 'do_date', - value: row.realization_date + cell: ({ row }) => + row.realization_date ? formatDate(row.realization_date, 'DD MMM YY') : '-', - }, - { key: 'aging', value: row.aging_days ?? '-' }, - { key: 'warehouse', value: row.warehouse?.name ?? '-' }, - { key: 'customer', value: row.customer?.name ?? '-' }, - { key: 'sales', value: row.sales?.name ?? '-' }, - { key: 'product', value: row.product?.name ?? '-' }, - { key: 'do_number', value: row.do_number ?? '-' }, - { key: 'vehicle', value: row.vehicle_number ?? '-' }, - { - key: 'marketing_type', - value: row.marketing_type ? ( + }, + { + key: 'aging', + header: 'Aging (Hari)', + flex: 0.7, + align: 'center', + cell: ({ row }) => row.aging_days ?? '-', + }, + { + key: 'warehouse', + header: 'Gudang', + flex: 1.2, + align: 'left', + cell: ({ row }) => row.warehouse?.name ?? '-', + }, + { + key: 'customer', + header: 'Pelanggan', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.customer?.name ?? '-', + }, + { + key: 'sales', + header: 'Sales', + flex: 1, + align: 'left', + cell: ({ row }) => row.sales?.name ?? '-', + }, + { + key: 'product', + header: 'Produk', + flex: 1.3, + align: 'left', + cell: ({ row }) => row.product?.name ?? '-', + }, + { + key: 'do_number', + header: 'Nomor DO', + flex: 1.2, + align: 'left', + cell: ({ row }) => row.do_number ?? '-', + }, + { + key: 'vehicle', + header: 'Nomor Polisi', + flex: 1, + align: 'left', + cell: ({ row }) => row.vehicle_number ?? '-', + }, + { + key: 'marketing_type', + header: 'Tipe Marketing', + flex: 1, + align: 'center', + cell: ({ row }) => + row.marketing_type ? ( { ) : ( '-' ), - }, - { key: 'qty', value: formatNumber(row.qty ?? 0), align: 'right' }, - { - key: 'avg_weight', - value: formatNumber(row.average_weight_kg ?? 0), - align: 'right', - }, - { - key: 'total_weight', - value: formatNumber(row.total_weight_kg ?? 0), - align: 'right', - }, - { - key: 'sales_price', - value: formatCurrency(row.sales_price_per_kg ?? 0), - align: 'right', - }, - { - key: 'hpp_price', - value: formatCurrency(row.hpp_price_per_kg ?? 0), - align: 'right', - }, - { - key: 'sales_amount', - value: formatCurrency(row.sales_amount ?? 0), - align: 'right', - }, - { - key: 'hpp_amount', - value: formatCurrency(row.hpp_amount ?? 0), - align: 'right', - }, - ]); -}; - -const getTableFooter = (summary?: SalesSummary): PdfTfootCell[] => { - if (!summary) return []; - - return [ - { key: 'no', value: 'TOTAL' }, - { key: 'so_date', value: '' }, - { key: 'do_date', value: '' }, - { key: 'aging', value: '' }, - { key: 'warehouse', value: '' }, - { key: 'customer', value: '' }, - { key: 'sales', value: '' }, - { key: 'product', value: '' }, - { key: 'do_number', value: '' }, - { key: 'vehicle', value: '' }, - { key: 'marketing_type', value: '' }, - { - key: 'qty', - value: formatNumber(summary.total_qty ?? 0), - align: 'right', - }, - { - key: 'avg_weight', - value: formatNumber(summary.total_weight_kg ?? 0), - align: 'right', - }, - { - key: 'total_weight', - value: formatNumber(summary.total_weight_kg ?? 0), - align: 'right', - }, - { key: 'sales_price', value: '' }, - { - key: 'hpp_price', - value: formatCurrency(summary.total_hpp_price_per_kg ?? 0), - align: 'right', - }, - { - key: 'sales_amount', - value: formatCurrency(summary.total_sales_amount ?? 0), - align: 'right', - }, - { - key: 'hpp_amount', - value: formatCurrency(summary.total_hpp_amount ?? 0), - align: 'right', - }, - ]; -}; + }, + { + key: 'qty', + header: 'Quantity', + flex: 0.7, + align: 'right', + cell: ({ row }) => formatNumber(row.qty ?? 0), + footer: summary ? formatNumber(summary.total_qty ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'avg_weight', + header: 'Rata-Rata (Kg)', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.average_weight_kg ?? 0), + footer: summary ? formatNumber(summary.total_weight_kg ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'total_weight', + header: 'Total Berat (Kg)', + flex: 0.9, + align: 'right', + cell: ({ row }) => formatNumber(row.total_weight_kg ?? 0), + footer: summary ? formatNumber(summary.total_weight_kg ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'sales_price', + header: 'Harga Jual (Rp)', + flex: 0.9, + align: 'right', + cell: ({ row }) => formatCurrency(row.sales_price_per_kg ?? 0), + footer: '', + }, + { + key: 'hpp_price', + header: 'HPP (Rp)', + flex: 1.3, + align: 'right', + cell: ({ row }) => formatCurrency(row.hpp_price_per_kg ?? 0), + footer: summary ? formatCurrency(summary.total_hpp_price_per_kg ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'sales_amount', + header: 'Total Jual (Rp)', + flex: 1, + align: 'right', + cell: ({ row }) => formatCurrency(row.sales_amount ?? 0), + footer: summary ? formatCurrency(summary.total_sales_amount ?? 0) : '', + footerAlign: 'right', + }, + { + key: 'hpp_amount', + header: 'Total HPP (Rp)', + flex: 1.3, + align: 'right', + cell: ({ row }) => formatCurrency(row.hpp_amount ?? 0), + footer: summary ? formatCurrency(summary.total_hpp_amount ?? 0) : '', + footerAlign: 'right', + }, +]; const DailyMarketingReportPDF = ({ data, @@ -249,9 +255,9 @@ const DailyMarketingReportPDF = ({ {/* Table */} diff --git a/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx index d9f33f27..f2a3c835 100644 --- a/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx +++ b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx @@ -14,12 +14,7 @@ import { HppPerKandangPerWeightRange, } from '@/types/api/report/hpp-per-kandang'; import { formatDate, formatNumber, formatCurrency } from '@/lib/helper'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, - PdfTfootCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; @@ -70,234 +65,185 @@ const formatSuppliers = ( }; // Helper functions for PdfTable - Rekapitulasi -const getRekapitulasiColumns = (): PdfColumn[] => [ - { key: 'rentang_bw', header: 'Rentang BW', flex: 1.2, align: 'center' }, - { key: 'sisa_butir', header: 'Sisa Butir', flex: 1, align: 'right' }, - { key: 'sisa_kg', header: 'Sisa Kg', flex: 1, align: 'right' }, +const getRekapitulasiColumns = (): PdfColumn[] => [ + { + key: 'rentang_bw', + header: 'Rentang BW', + flex: 1.2, + align: 'center', + cell: ({ row }) => row.label || '-', + }, + { + key: 'sisa_butir', + header: 'Sisa Butir', + flex: 1, + align: 'right', + cell: ({ row }) => formatNumber(row.egg_production_pieces || 0), + }, + { + key: 'sisa_kg', + header: 'Sisa Kg', + flex: 1, + align: 'right', + cell: ({ row }) => formatNumber(row.egg_production_kg || 0), + }, { key: 'rata_rata_bobot', header: 'Rata-Rata Bobot (Kg)', flex: 1.2, align: 'right', + cell: ({ row }) => formatNumber(row.avg_weight_kg || 0), }, { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.5, align: 'left', + cell: ({ row }) => formatSuppliers(row.feed_suppliers), }, { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1.2, align: 'left', + cell: ({ row }) => formatSuppliers(row.doc_suppliers), }, { key: 'rata_harga_doc', header: 'Rata-Rata Harga DOC', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.average_doc_price_rp || 0), }, { key: 'hpp_telur', header: 'HPP Telur (Rp/Kg)', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.egg_hpp_rp_per_kg || 0), }, { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.egg_value_rp || 0), }, ]; -const getRekapitulasiData = ( - perWeightRange: HppPerKandangPerWeightRange[] -): PdfTbodyCell[][] => { - return perWeightRange.map((group) => [ - { key: 'rentang_bw', value: group.label || '-' }, - { - key: 'sisa_butir', - value: formatNumber(group.egg_production_pieces || 0), - align: 'right', - }, - { - key: 'sisa_kg', - value: formatNumber(group.egg_production_kg || 0), - align: 'right', - }, - { - key: 'rata_rata_bobot', - value: formatNumber(group.avg_weight_kg || 0), - align: 'right', - }, - { - key: 'feed_supplier', - value: formatSuppliers(group.feed_suppliers), - }, - { - key: 'doc_supplier', - value: formatSuppliers(group.doc_suppliers), - }, - { - key: 'rata_harga_doc', - value: formatCurrency(group.average_doc_price_rp || 0), - align: 'right', - }, - { - key: 'hpp_telur', - value: formatCurrency(group.egg_hpp_rp_per_kg || 0), - align: 'right', - }, - { - key: 'nominal_sisa', - value: formatCurrency(group.egg_value_rp || 0), - align: 'right', - }, - ]); -}; - // Helper functions for PdfTable - Detail Per Kandang -const getDetailColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'kandang', header: 'Kandang', flex: 1.5, align: 'left' }, - { key: 'rentang_bw', header: 'Rentang BW', flex: 1, align: 'left' }, +const getDetailColumns = ( + summary?: HppPerKandangReport['summary'], + allFeedSuppliers?: string, + allDocSuppliers?: string +): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + footer: 'TOTAL', + }, + { + key: 'kandang', + header: 'Kandang', + flex: 1.5, + align: 'left', + cell: ({ row }) => row.kandang?.name || '-', + footer: 'ALL', + }, + { + key: 'rentang_bw', + header: 'Rentang BW', + flex: 1, + align: 'left', + cell: ({ row }) => + `${row.weight_range.weight_min.toFixed(2)} - ${row.weight_range.weight_max.toFixed(2)}`, + footer: '-', + }, { key: 'rata_rata_bobot', header: 'Rata-Rata Bobot (Kg)', flex: 1, align: 'right', + cell: ({ row }) => formatNumber(row.avg_weight_kg || 0), + footer: summary ? formatNumber(summary.total.average_weight_kg || 0) : '', + footerAlign: 'right', + }, + { + key: 'sisa_butir', + header: 'Sisa Butir', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.egg_production_pieces || 0), + footer: summary + ? formatNumber(summary.total.total_egg_production_pieces || 0) + : '', + footerAlign: 'right', + }, + { + key: 'sisa_kg', + header: 'Sisa Kg (Telur)', + flex: 0.8, + align: 'right', + cell: ({ row }) => formatNumber(row.egg_production_kg || 0), + footer: summary + ? formatNumber(summary.total.total_egg_production_kg || 0) + : '', + footerAlign: 'right', }, - { key: 'sisa_butir', header: 'Sisa Butir', flex: 0.8, align: 'right' }, - { key: 'sisa_kg', header: 'Sisa Kg (Telur)', flex: 0.8, align: 'right' }, { key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.2, align: 'left', + cell: ({ row }) => formatSuppliers(row.feed_suppliers), + footer: allFeedSuppliers || '-', }, { key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1, align: 'left', + cell: ({ row }) => formatSuppliers(row.doc_suppliers), + footer: allDocSuppliers || '-', }, { key: 'rata_harga_doc', header: 'Rata-Rata Harga DOC', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.average_doc_price_rp || 0), + footer: summary + ? formatCurrency(summary.total.total_average_doc_price_rp || 0) + : '', + footerAlign: 'right', }, { key: 'hpp_telur', header: 'HPP Telur (Rp/Kg)', flex: 1, align: 'right', + cell: ({ row }) => formatCurrency(row.egg_hpp_rp_per_kg || 0), + footer: summary + ? formatCurrency(summary.total.average_egg_hpp_rp_per_kg || 0) + : '', + footerAlign: 'right', }, { key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right', + cell: ({ row }) => formatCurrency(row.egg_value_rp || 0), + footer: summary + ? formatCurrency(summary.total.total_egg_value_rp || 0) + : '', + footerAlign: 'right', }, ]; -const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { - return rows.map((item, index) => [ - { key: 'no', value: index + 1 }, - { key: 'kandang', value: item.kandang?.name || '-' }, - { - key: 'rentang_bw', - value: `${item.weight_range.weight_min.toFixed(2)} - ${item.weight_range.weight_max.toFixed(2)}`, - }, - { - key: 'rata_rata_bobot', - value: formatNumber(item.avg_weight_kg || 0), - align: 'right', - }, - { - key: 'sisa_butir', - value: formatNumber(item.egg_production_pieces || 0), - align: 'right', - }, - { - key: 'sisa_kg', - value: formatNumber(item.egg_production_kg || 0), - align: 'right', - }, - { - key: 'feed_supplier', - value: formatSuppliers(item.feed_suppliers), - }, - { - key: 'doc_supplier', - value: formatSuppliers(item.doc_suppliers), - }, - { - key: 'rata_harga_doc', - value: formatCurrency(item.average_doc_price_rp || 0), - align: 'right', - }, - { - key: 'hpp_telur', - value: formatCurrency(item.egg_hpp_rp_per_kg || 0), - align: 'right', - }, - { - key: 'nominal_sisa', - value: formatCurrency(item.egg_value_rp || 0), - align: 'right', - }, - ]); -}; - -const getDetailFooter = ( - summary: HppPerKandangReport['summary'], - allFeedSuppliers: string, - allDocSuppliers: string -): PdfTfootCell[] => { - if (!summary?.total) return []; - - return [ - { key: 'no', value: 'TOTAL' }, - { key: 'kandang', value: 'ALL' }, - { key: 'rentang_bw', value: '-' }, - { - key: 'rata_rata_bobot', - value: formatNumber(summary.total.average_weight_kg || 0), - align: 'right', - }, - { - key: 'sisa_butir', - value: formatNumber(summary.total.total_egg_production_pieces || 0), - align: 'right', - }, - { - key: 'sisa_kg', - value: formatNumber(summary.total.total_egg_production_kg || 0), - align: 'right', - }, - { key: 'feed_supplier', value: allFeedSuppliers }, - { key: 'doc_supplier', value: allDocSuppliers }, - { - key: 'rata_harga_doc', - value: formatCurrency(summary.total.total_average_doc_price_rp || 0), - align: 'right', - }, - { - key: 'hpp_telur', - value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg || 0), - align: 'right', - }, - { - key: 'nominal_sisa', - value: formatCurrency(summary.total.total_egg_value_rp || 0), - align: 'right', - }, - ]; -}; - const createPDFDocument = ( params: HppPerKandangExportParams, allFeedSuppliers: string, @@ -355,7 +301,7 @@ const createPDFDocument = ( @@ -365,17 +311,13 @@ const createPDFDocument = ( Detail Per Kandang
diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx index 6f7b8313..eabb03bf 100644 --- a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -9,11 +9,7 @@ import { ProductionResult } from '@/types/api/report/production-result'; import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber'; -import { - PdfTable, - PdfColumn, - PdfTbodyCell, -} from '@/components/helper/pdf/table'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; type MappedProductionResultsItem = { projectFlockKandang: BaseProjectFlockKandang; @@ -64,246 +60,339 @@ function valueText(v: unknown) { // ======================================== // TABLE 1: WOA & BW // ======================================== -const getBwTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'woa', header: 'Week of Age', flex: 0.8, align: 'center' }, - { key: 'bw', header: 'Body Weight', flex: 1, align: 'right' }, - { key: 'std_bw', header: 'Std Body Weight', flex: 1, align: 'right' }, - { key: 'uniformity', header: 'Uniformity', flex: 1.2, align: 'right' }, +const getBwTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'woa', + header: 'Week of Age', + flex: 0.8, + align: 'center', + cell: ({ row }) => valueText(row.woa), + }, + { + key: 'bw', + header: 'Body Weight', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.bw), + }, + { + key: 'std_bw', + header: 'Std Body Weight', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.std_bw), + }, + { + key: 'uniformity', + header: 'Uniformity', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.uniformity), + }, { key: 'std_uniformity', header: 'Std Uniformity', flex: 1.3, align: 'right', + cell: ({ row }) => valueText(row.std_uniformity), }, ]; -const getBwTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'woa', value: valueText(pr.woa) }, - { key: 'bw', value: valueText(pr.bw), align: 'right' }, - { key: 'std_bw', value: valueText(pr.std_bw), align: 'right' }, - { key: 'uniformity', value: valueText(pr.uniformity), align: 'right' }, - { - key: 'std_uniformity', - value: valueText(pr.std_uniformity), - align: 'right', - }, - ]; - }); -}; - // ======================================== // TABLE 2: DEPLESI // ======================================== -const getDepTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, +const getDepTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, { key: 'dep_kum', header: 'Depletion Cummulative', flex: 1.5, align: 'right', + cell: ({ row }) => valueText(row.dep_kum), + }, + { + key: 'dep_std', + header: 'Depletion Std', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.dep_std), }, - { key: 'dep_std', header: 'Depletion Std', flex: 1.5, align: 'right' }, ]; -const getDepTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'dep_kum', value: valueText(pr.dep_kum), align: 'right' }, - { key: 'dep_std', value: valueText(pr.dep_std), align: 'right' }, - ]; - }); -}; - // ======================================== // TABLE 3: BUTIRAN // ======================================== -const getButiranTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'butiran_utuh', header: 'Utuh', flex: 1.2, align: 'right' }, - { key: 'butiran_putih', header: 'Putih', flex: 1.2, align: 'right' }, - { key: 'butiran_retak', header: 'Retak', flex: 1.2, align: 'right' }, - { key: 'butiran_pecah', header: 'Pecah', flex: 1.2, align: 'right' }, - { key: 'butiran_jumlah', header: 'Jumlah', flex: 1.2, align: 'right' }, - { key: 'total_butir', header: 'Total Butir', flex: 1.3, align: 'right' }, +const getButiranTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'butiran_utuh', + header: 'Utuh', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_utuh), + }, + { + key: 'butiran_putih', + header: 'Putih', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_putih), + }, + { + key: 'butiran_retak', + header: 'Retak', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_retak), + }, + { + key: 'butiran_pecah', + header: 'Pecah', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_pecah), + }, + { + key: 'butiran_jumlah', + header: 'Jumlah', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.butiran_jumlah), + }, + { + key: 'total_butir', + header: 'Total Butir', + flex: 1.3, + align: 'right', + cell: ({ row }) => valueText(row.total_butir), + }, ]; -const getButiranTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { - key: 'butiran_utuh', - value: valueText(pr.butiran_utuh), - align: 'right', - }, - { - key: 'butiran_putih', - value: valueText(pr.butiran_putih), - align: 'right', - }, - { - key: 'butiran_retak', - value: valueText(pr.butiran_retak), - align: 'right', - }, - { - key: 'butiran_pecah', - value: valueText(pr.butiran_pecah), - align: 'right', - }, - { - key: 'butiran_jumlah', - value: valueText(pr.butiran_jumlah), - align: 'right', - }, - { - key: 'total_butir', - value: valueText(pr.total_butir), - align: 'right', - }, - ]; - }); -}; - // ======================================== // TABLE 4: BERAT (KG) // ======================================== -const getKgTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'kg_utuh', header: 'Utuh (Kg)', flex: 1.2, align: 'right' }, - { key: 'kg_putih', header: 'Putih (Kg)', flex: 1.2, align: 'right' }, - { key: 'kg_retak', header: 'Retak (Kg)', flex: 1.2, align: 'right' }, - { key: 'kg_pecah', header: 'Pecah (Kg)', flex: 1.2, align: 'right' }, - { key: 'kg_jumlah', header: 'Jumlah (Kg)', flex: 1.3, align: 'right' }, - { key: 'total_kg', header: 'Total (Kg)', flex: 1.3, align: 'right' }, +const getKgTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'kg_utuh', + header: 'Utuh (Kg)', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.kg_utuh), + }, + { + key: 'kg_putih', + header: 'Putih (Kg)', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.kg_putih), + }, + { + key: 'kg_retak', + header: 'Retak (Kg)', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.kg_retak), + }, + { + key: 'kg_pecah', + header: 'Pecah (Kg)', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.kg_pecah), + }, + { + key: 'kg_jumlah', + header: 'Jumlah (Kg)', + flex: 1.3, + align: 'right', + cell: ({ row }) => valueText(row.kg_jumlah), + }, + { + key: 'total_kg', + header: 'Total (Kg)', + flex: 1.3, + align: 'right', + cell: ({ row }) => valueText(row.total_kg), + }, ]; -const getKgTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'kg_utuh', value: valueText(pr.kg_utuh), align: 'right' }, - { key: 'kg_putih', value: valueText(pr.kg_putih), align: 'right' }, - { key: 'kg_retak', value: valueText(pr.kg_retak), align: 'right' }, - { key: 'kg_pecah', value: valueText(pr.kg_pecah), align: 'right' }, - { key: 'kg_jumlah', value: valueText(pr.kg_jumlah), align: 'right' }, - { key: 'total_kg', value: valueText(pr.total_kg), align: 'right' }, - ]; - }); -}; - // ======================================== // TABLE 5: PERSENTASE // ======================================== -const getPersenTableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'persen_utuh', header: 'Utuh (%)', flex: 1.5, align: 'right' }, - { key: 'persen_putih', header: 'Putih (%)', flex: 1.5, align: 'right' }, - { key: 'persen_retak', header: '% Retak (%)', flex: 1.5, align: 'right' }, - { key: 'persen_pecah', header: '% Pecah (%)', flex: 1.5, align: 'right' }, +const getPersenTableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'persen_utuh', + header: 'Utuh (%)', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.persen_utuh), + }, + { + key: 'persen_putih', + header: 'Putih (%)', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.persen_putih), + }, + { + key: 'persen_retak', + header: '% Retak (%)', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.persen_retak), + }, + { + key: 'persen_pecah', + header: '% Pecah (%)', + flex: 1.5, + align: 'right', + cell: ({ row }) => valueText(row.persen_pecah), + }, ]; -const getPersenTableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { - key: 'persen_utuh', - value: valueText(pr.persen_utuh), - align: 'right', - }, - { - key: 'persen_putih', - value: valueText(pr.persen_putih), - align: 'right', - }, - { - key: 'persen_retak', - value: valueText(pr.persen_retak), - align: 'right', - }, - { - key: 'persen_pecah', - value: valueText(pr.persen_pecah), - align: 'right', - }, - ]; - }); -}; - // ======================================== // TABLE 6: PRODUKSI (HD, FI, EM, EW) // ======================================== -const getProduksi1TableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'hd', header: 'Hen Day', flex: 0.8, align: 'right' }, - { key: 'hd_std', header: 'Hen Day Std', flex: 1, align: 'right' }, - { key: 'fi', header: 'Feed Intake', flex: 0.8, align: 'right' }, - { key: 'fi_std', header: 'Feed Intake Std', flex: 1, align: 'right' }, - { key: 'em', header: 'Egg Mass', flex: 0.8, align: 'right' }, - { key: 'em_std', header: 'Egg Mass Std', flex: 1, align: 'right' }, - { key: 'ew', header: 'Egg Weight', flex: 0.8, align: 'right' }, - { key: 'ew_std', header: 'Egg Weight Std', flex: 1, align: 'right' }, +const getProduksi1TableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'hd', + header: 'Hen Day', + flex: 0.8, + align: 'right', + cell: ({ row }) => valueText(row.hd), + }, + { + key: 'hd_std', + header: 'Hen Day Std', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.hd_std), + }, + { + key: 'fi', + header: 'Feed Intake', + flex: 0.8, + align: 'right', + cell: ({ row }) => valueText(row.fi), + }, + { + key: 'fi_std', + header: 'Feed Intake Std', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.fi_std), + }, + { + key: 'em', + header: 'Egg Mass', + flex: 0.8, + align: 'right', + cell: ({ row }) => valueText(row.em), + }, + { + key: 'em_std', + header: 'Egg Mass Std', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.em_std), + }, + { + key: 'ew', + header: 'Egg Weight', + flex: 0.8, + align: 'right', + cell: ({ row }) => valueText(row.ew), + }, + { + key: 'ew_std', + header: 'Egg Weight Std', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.ew_std), + }, ]; -const getProduksi1TableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'hd', value: valueText(pr.hd), align: 'right' }, - { key: 'hd_std', value: valueText(pr.hd_std), align: 'right' }, - { key: 'fi', value: valueText(pr.fi), align: 'right' }, - { key: 'fi_std', value: valueText(pr.fi_std), align: 'right' }, - { key: 'em', value: valueText(pr.em), align: 'right' }, - { key: 'em_std', value: valueText(pr.em_std), align: 'right' }, - { key: 'ew', value: valueText(pr.ew), align: 'right' }, - { key: 'ew_std', value: valueText(pr.ew_std), align: 'right' }, - ]; - }); -}; - // ======================================== // TABLE 7: PRODUKSI (FCR, HH) // ======================================== -const getProduksi2TableColumns = (): PdfColumn[] => [ - { key: 'no', header: 'No', flex: 0.5, align: 'center' }, - { key: 'fcr', header: 'FCR', flex: 1, align: 'right' }, - { key: 'fcr_std', header: 'FCR Std', flex: 1.2, align: 'right' }, - { key: 'hh', header: 'Hen House', flex: 1, align: 'right' }, - { key: 'hh_std', header: 'Hen House Std', flex: 1.2, align: 'right' }, +const getProduksi2TableColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ row, index }) => index + 1, + }, + { + key: 'fcr', + header: 'FCR', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.fcr), + }, + { + key: 'fcr_std', + header: 'FCR Std', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.fcr_std), + }, + { + key: 'hh', + header: 'Hen House', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.hh), + }, + { + key: 'hh_std', + header: 'Hen House Std', + flex: 1.2, + align: 'right', + cell: ({ row }) => valueText(row.hh_std), + }, ]; -const getProduksi2TableData = ( - productionResults: ProductionResult[] -): PdfTbodyCell[][] => { - return productionResults.map((pr, index) => { - return [ - { key: 'no', value: index + 1 }, - { key: 'fcr', value: valueText(pr.fcr), align: 'right' }, - { key: 'fcr_std', value: valueText(pr.fcr_std), align: 'right' }, - { key: 'hh', value: valueText(pr.hh), align: 'right' }, - { key: 'hh_std', value: valueText(pr.hh_std), align: 'right' }, - ]; - }); -}; - /** * ✅ Main PDF Component */ @@ -383,7 +472,7 @@ const ProductionResultReportPDF = ({ 1. WOA & Body Weight
@@ -392,7 +481,7 @@ const ProductionResultReportPDF = ({ 2. Deplesi @@ -401,7 +490,7 @@ const ProductionResultReportPDF = ({ 3. Butiran @@ -410,7 +499,7 @@ const ProductionResultReportPDF = ({ 4. Berat (Kg) @@ -419,7 +508,7 @@ const ProductionResultReportPDF = ({ 5. Persentase @@ -430,7 +519,7 @@ const ProductionResultReportPDF = ({ @@ -439,7 +528,7 @@ const ProductionResultReportPDF = ({ 7. Produksi (FCR, HH) From 539de03a5be11b9feb466b7db8bcb336bc4ff068 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 10:41:57 +0700 Subject: [PATCH 054/149] refactor(FE): Update section width to use full width --- src/app/report/production-result/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/report/production-result/page.tsx b/src/app/report/production-result/page.tsx index 691ea734..cdac598c 100644 --- a/src/app/report/production-result/page.tsx +++ b/src/app/report/production-result/page.tsx @@ -2,7 +2,7 @@ import ProductionResultContent from '@/components/pages/report/production-result const ProductionResultReportPage = () => { return ( -
+
); From 3834982fca1addce8f62f709a30d449756f72555 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 11:40:29 +0700 Subject: [PATCH 055/149] feat(FE): Add "has_chickin" property to disable quantity editing --- .../form/order/PurchaseOrderAcceptApprovalForm.tsx | 14 +++++++++++++- .../form/order/PurchaseOrderStaffApprovalForm.tsx | 14 +++++++++++++- src/types/api/purchase/purchase.d.ts | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index c7b196a2..f872e7e0 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -402,6 +402,13 @@ const PurchaseOrderAcceptApprovalForm = ({ {purchaseItems?.map((purchaseItem, idx) => { const formItem = formik.values.items?.[idx]; + + const originalPurchaseItem = initialValues?.items?.find( + (item) => item.id === purchaseItem.id + ); + const isReceivedQtyDisabled = + originalPurchaseItem?.has_chickin === true; + return ( @@ -580,7 +587,12 @@ const PurchaseOrderAcceptApprovalForm = ({ decimalScale={0} thousandSeparator=',' decimalSeparator='.' - bottomLabel={`Total: ${purchaseItems[idx]?.quantity ? formatNumber(purchaseItems[idx].quantity) : 0}`} + disabled={isReceivedQtyDisabled} + bottomLabel={ + isReceivedQtyDisabled + ? 'Sudah chickin, tidak bisa diubah' + : `Total: ${purchaseItems[idx]?.quantity ? formatNumber(purchaseItems[idx].quantity) : 0}` + } isError={ isRepeaterInputError(idx, 'received_qty').isError || (formItem?.received_qty diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 000c212b..1e674f4f 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -740,6 +740,13 @@ const PurchaseOrderStaffApprovalForm = ({ ) return null; + const originalPurchaseItem = + initialValues?.items?.find( + (item) => item.id === purchaseItem.id + ); + const isQtyDisabled = + originalPurchaseItem?.has_chickin === true; + return ( @@ -807,7 +814,12 @@ const PurchaseOrderStaffApprovalForm = ({ placeholder='Masukkan jumlah' allowNegative={false} decimalScale={0} - bottomLabel={`Previous: ${formatNumber(purchaseItem.quantity)}`} + disabled={isQtyDisabled} + bottomLabel={ + isQtyDisabled + ? 'Sudah chickin, tidak bisa diubah' + : `Previous: ${formatNumber(purchaseItem.quantity)}` + } className={{ wrapper: 'min-w-32', }} diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index f64d4275..d39719a3 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -50,6 +50,7 @@ export type PurchaseItem = { expedition_vendor_name?: string | null; received_qty?: number | null; transport_per_item?: number | null; + has_chickin?: boolean; expedition_vendor?: { id?: number; name?: string; From c3dee6b2929b7767157657c15d68d56042cc2544 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 13:42:56 +0700 Subject: [PATCH 056/149] feat(FE): Add Zustand store for ProjectFlock management --- .../project-flock/project-flock.store.ts | 19 +++++++++++++++ .../slices/project-flock.slice.ts | 24 +++++++++++++++++++ src/types/stores.d.ts | 12 ++++++++++ 3 files changed, 55 insertions(+) create mode 100644 src/stores/project-flock/project-flock.store.ts create mode 100644 src/stores/project-flock/slices/project-flock.slice.ts diff --git a/src/stores/project-flock/project-flock.store.ts b/src/stores/project-flock/project-flock.store.ts new file mode 100644 index 00000000..61efcb97 --- /dev/null +++ b/src/stores/project-flock/project-flock.store.ts @@ -0,0 +1,19 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createProjectFlockSlice } from '@/stores/project-flock/slices/project-flock.slice'; +import { ProjectFlockSlice } from '@/types/stores'; + +export type ProjectFlockStore = ProjectFlockSlice; + +export const useProjectFlockStore = create()( + devtools( + (...args) => ({ + ...createProjectFlockSlice(...args), + }), + { + name: 'ProjectFlockStore', + } + ) +); diff --git a/src/stores/project-flock/slices/project-flock.slice.ts b/src/stores/project-flock/slices/project-flock.slice.ts new file mode 100644 index 00000000..03f3205d --- /dev/null +++ b/src/stores/project-flock/slices/project-flock.slice.ts @@ -0,0 +1,24 @@ +import { ProjectFlockSlice } from '@/types/stores'; +import { StateCreator } from 'zustand'; + +export const createProjectFlockSlice: StateCreator< + ProjectFlockSlice, + [], + [], + ProjectFlockSlice +> = (set) => ({ + // Initial state + isSuccess: false, + createdProjectFlock: null, + + // Actions + setIsSuccess: (success) => set({ isSuccess: success }), + + setCreatedProjectFlock: (data) => set({ createdProjectFlock: data }), + + resetProjectFlock: () => + set({ + isSuccess: false, + createdProjectFlock: null, + }), +}); diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index 47d2c1fd..c358e2a1 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -5,6 +5,7 @@ import type { UniformityDetail, VerifyUniformityResponse, } from '@/types/api/production/uniformity'; +import type { ProjectFlock } from '@/types/api/production/project-flock'; type MainUiSlice = { mainDrawerOpen: boolean; @@ -97,3 +98,14 @@ export type DashboardFilterSlice = { setFilterValues: (values: DashboardFilterType) => void; resetFilterValues: () => void; }; + +export type ProjectFlockSlice = { + // State + isSuccess: boolean; + createdProjectFlock: ProjectFlock | null; + + // Actions + setIsSuccess: (success: boolean) => void; + setCreatedProjectFlock: (data: ProjectFlock | null) => void; + resetProjectFlock: () => void; +}; From 4215b0ea7d26282d48098f1b4918245929c8f720 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 13:44:00 +0700 Subject: [PATCH 057/149] feat(FE): Add success modal and state management for ProjectFlock --- .../project-flock/ProjectFlockTable.tsx | 81 +++++++++++++++++++ .../project-flock/form/ProjectFlockForm.tsx | 30 +++---- 2 files changed, 94 insertions(+), 17 deletions(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 7c4973eb..4085bc56 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -33,6 +33,9 @@ import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; +import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal'; +import { useProjectFlockStore } from '@/stores/project-flock/project-flock.store'; +import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; const RowOptionsMenu = ({ props, @@ -137,6 +140,15 @@ const RowOptionsMenu = ({ }; const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { + const isSuccess = useProjectFlockStore((s) => s.isSuccess); + const setIsSuccess = useProjectFlockStore((s) => s.setIsSuccess); + const createdProjectFlock = useProjectFlockStore( + (s) => s.createdProjectFlock + ); + const setCreatedProjectFlock = useProjectFlockStore( + (s) => s.setCreatedProjectFlock + ); + const { state: tableFilterState, updateFilter, @@ -180,6 +192,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const [sorting, setSorting] = useState([]); const deleteModal = useModal(); const confirmModal = useModal(); + const successModal = useModal(); const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' ); @@ -275,6 +288,64 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { refreshProjectFlocks(); }, [refresh]); + useEffect(() => { + if (isSuccess) { + successModal.openModal(); + } + }, [isSuccess, successModal]); + + const handleSuccessModalClose = () => { + successModal.closeModal(); + setIsSuccess(false); + setCreatedProjectFlock(null); + }; + + const projectFlockFormValues = useMemo(() => { + if (!createdProjectFlock) return undefined; + + return { + flock: { + value: 0, + label: createdProjectFlock.flock_name || '', + }, + flock_name: createdProjectFlock.flock_name || '', + area: { + value: createdProjectFlock.area_id, + label: createdProjectFlock.area?.name || '', + }, + area_id: createdProjectFlock.area_id, + category_option: { + value: createdProjectFlock.category, + label: createdProjectFlock.category, + }, + category: createdProjectFlock.category, + production_standard: { + value: createdProjectFlock.production_standard_id, + label: createdProjectFlock.production_standard?.name || '', + }, + production_standard_id: createdProjectFlock.production_standard_id, + location: { + value: createdProjectFlock.location_id, + label: createdProjectFlock.location?.name || '', + }, + location_id: createdProjectFlock.location_id, + kandang_ids: createdProjectFlock.kandangs?.map((k) => k.id) || [], + project_budgets: + createdProjectFlock.project_budgets?.map((budget) => ({ + nonstock: budget.nonstock + ? { + value: budget.nonstock_id, + label: budget.nonstock.name || '', + } + : null, + nonstock_id: budget.nonstock_id, + qty: budget.qty, + price: budget.price, + total_price: budget.qty * budget.price, + })) || [], + } as ProjectFlockFormValues; + }, [createdProjectFlock]); + // ====== MEMO ====== const selectedSingleRow: ProjectFlock | null | undefined = useMemo(() => { return selectedRowIds.length === 1 @@ -873,6 +944,16 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { isLoading: isApproveLoading, }} /> + + ); }; diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 82c9e595..d56550a6 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -52,7 +52,7 @@ import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import StatusBadge from '@/components/helper/StatusBadge'; import { getUniqueFormikErrors } from '@/lib/formik-helper'; -import ProjectFlockConfirmationModal from '../ProjectFlockConfirmationModal'; +import { useProjectFlockStore } from '@/stores/project-flock/project-flock.store'; interface ProjectFlockFormProps { formType?: 'add' | 'edit' | 'detail'; @@ -213,6 +213,10 @@ const ProjectFlockForm = ({ }: ProjectFlockFormProps) => { // State const router = useRouter(); + const setIsSuccess = useProjectFlockStore((s) => s.setIsSuccess); + const setCreatedProjectFlock = useProjectFlockStore( + (s) => s.setCreatedProjectFlock + ); const [formStep, setFormStep] = useState<'form' | 'confirmation'>('form'); @@ -239,7 +243,6 @@ const ProjectFlockForm = ({ const subscribeValidate = useUiStore((s) => s.subscribeValidate); const setIsValid = useUiStore((s) => s.setIsValid); - const successModal = useModal(); const deleteModal = useModal(); const [isDeleteLoading, setIsDeleteLoading] = useState(false); @@ -262,6 +265,8 @@ const ProjectFlockForm = ({ loadMore: loadMoreFlock, } = useSelect(FlockApi.basePath, 'id', 'name', '', { project_category: selectedCategory, + location_id: selectedLocation, + area_id: selectedArea, }); const { @@ -431,7 +436,9 @@ const ProjectFlockForm = ({ if (isResponseSuccess(createProjectFlockRes)) { toast.success(createProjectFlockRes?.message as string); handleReset(); - successModal.openModal(); + setCreatedProjectFlock(createProjectFlockRes?.data ?? null); + setIsSuccess(true); + router.push('/production/project-flock'); } if (isResponseError(createProjectFlockRes)) { setProjectFlockFormErrorMessage(createProjectFlockRes?.message as string); @@ -449,7 +456,9 @@ const ProjectFlockForm = ({ if (isResponseSuccess(updateProjectFlockRes)) { toast.success(updateProjectFlockRes?.message as string); handleReset(); - successModal.openModal(); + setCreatedProjectFlock(updateProjectFlockRes?.data ?? null); + setIsSuccess(true); + router.push('/production/project-flock'); } if (isResponseError(updateProjectFlockRes)) { setProjectFlockFormErrorMessage(updateProjectFlockRes?.message as string); @@ -1388,19 +1397,6 @@ const ProjectFlockForm = ({
- { - router.push('/production/project-flock'); - setFormikLastValues(undefined); - }} - secondaryButton={undefined} - /> - Date: Wed, 11 Feb 2026 13:54:48 +0700 Subject: [PATCH 058/149] refactor(FE): Improve disabled and read-only states for input components --- src/components/input/TextInput.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx index 89d4f059..39c5b54a 100644 --- a/src/components/input/TextInput.tsx +++ b/src/components/input/TextInput.tsx @@ -182,10 +182,12 @@ const TextInput = ({ ) : (
From 498602a2c908df27717dbe3435120b1be7b20a13 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 14:19:52 +0700 Subject: [PATCH 059/149] refactor(FE): Refactor input components to use consistent color variables --- src/components/input/SelectInput.tsx | 26 ++++++++++--------- src/components/input/TextInput.tsx | 17 ++++++------ .../recording/form/RecordingForm.tsx | 4 +++ 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index a79054dd..38be09e4 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -246,8 +246,8 @@ const SelectInput = (props: SelectInputProps) => { className={cn( 'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200', { - 'bg-gray-100 border-base-content/10': !isDisabled, - 'bg-gray-50 border-base-content/10': isDisabled, + 'bg-base-100 border-base-content/10': !isDisabled, + 'bg-base-200 border-base-content/10': isDisabled, 'border-error': isError, }, className?.inputPrefix @@ -278,15 +278,16 @@ const SelectInput = (props: SelectInputProps) => { className={cn('w-full flex-1', className?.select)} classNames={{ control: ({ isFocused, isDisabled }) => - cn('w-full border bg-white transition-shadow', 'rounded-lg!', { + cn('w-full border transition-shadow', 'rounded-lg!', { + 'bg-base-100!': !isDisabled && !readOnly, + 'bg-base-200! text-gray-400 cursor-not-allowed': + isDisabled && !readOnly, + 'bg-transparent! cursor-not-allowed!': readOnly, 'cursor-pointer!': !readOnly && !isDisabled, 'border-red-500! ring-2 ring-red-200': isError, 'border-indigo-500 ring-2 ring-indigo-200': isFocused && !startAdornment, 'border-base-content/10!': !isError && !isFocused, - 'bg-gray-100 text-gray-400 cursor-not-allowed': - isDisabled && !readOnly, - 'bg-transparent! cursor-not-allowed!': readOnly, 'rounded-l-none!': inputPrefix && !startAdornment, 'rounded-r-none!': inputSuffix && !startAdornment, }), @@ -370,8 +371,8 @@ const SelectInput = (props: SelectInputProps) => { className={cn( 'inline-flex items-center px-3 border border-l-0 border-base-content/10 rounded-r-lg transition-all duration-200', { - 'bg-gray-100 border-base-content/10': !isDisabled, - 'bg-gray-50 border-base-content/10': isDisabled, + 'bg-base-100 border-base-content/10': !isDisabled, + 'bg-base-200 border-base-content/10': isDisabled, 'border-error': isError, }, className?.inputSuffix @@ -404,18 +405,19 @@ const SelectInput = (props: SelectInputProps) => { classNames={{ control: ({ isFocused, isDisabled }) => cn( - 'w-full border bg-white transition-shadow', + 'w-full border transition-shadow', // Gunakan rounded-lg untuk semua kasus 'rounded-lg!', { + 'bg-base-100!': !isDisabled && !readOnly, + 'bg-base-200! text-gray-400 cursor-not-allowed': + isDisabled && !readOnly, + 'bg-transparent! cursor-not-allowed!': readOnly, 'cursor-pointer!': !readOnly && !isDisabled, 'border-red-500! ring-2 ring-red-200': isError, 'border-indigo-500 ring-2 ring-indigo-200': isFocused && !startAdornment, 'border-base-content/10!': !isError && !isFocused, - 'bg-gray-100 text-gray-400 cursor-not-allowed': - isDisabled && !readOnly, - 'bg-transparent! cursor-not-allowed!': readOnly, } ), valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx index 39c5b54a..b7d0984f 100644 --- a/src/components/input/TextInput.tsx +++ b/src/components/input/TextInput.tsx @@ -104,8 +104,8 @@ const TextInput = ({ className={cn( 'inline-flex items-center px-3 border border-r-0 border-base-content/10 rounded-l-lg transition-all duration-200', { - 'bg-gray-100 border-base-content/10': !disabled, - 'bg-gray-50 border-base-content/10': disabled, + 'bg-base-100 border-base-content/10': !disabled, + 'bg-base-200 border-base-content/10': disabled, 'border-error': isError, 'border-success!': isValid, }, @@ -118,7 +118,7 @@ const TextInput = ({
{ ) : null } + disabled={type === 'detail'} /> {getStockUsageAdornment(idx)}
@@ -2675,6 +2676,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ) : null } + disabled={type === 'detail'} /> {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( @@ -2891,6 +2893,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }} placeholder='Masukkan jumlah telur' inputSuffix={'Butir'} + disabled={type === 'detail'} /> @@ -2917,6 +2920,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }} placeholder='Masukkan total berat telur (Kilogram)...' inputSuffix='Kilogram' + disabled={type === 'detail'} /> {(type as 'add' | 'edit' | 'detail') !== From 4e5745d23765d96963e5418b6cbf30412815ad2c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 15:53:32 +0700 Subject: [PATCH 060/149] refactor(FE): Add tab state management and skeleton for PurchasesPerSupplierTab --- .../logistic-stock/LogisticStockTabs.tsx | 23 +- .../skeleton/PurchasePerSupplierSkeleton.tsx | 37 + .../tab/PurchasesPerSupplierTab.tsx | 994 +++++++++++------- .../logistic-stock-tab.store.ts | 51 + 4 files changed, 712 insertions(+), 393 deletions(-) create mode 100644 src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx create mode 100644 src/stores/logistic-stock-tab/logistic-stock-tab.store.ts diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index 1e2d2824..a7b844f3 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -1,14 +1,19 @@ 'use client'; +import { useState } from 'react'; import Tabs from '@/components/Tabs'; import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; +import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; const LogisticStockTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useLogisticStockTabStore((state) => state.tabActions); + const tabs = [ { id: '1', label: 'Rekapitulasi Pembelian Per Supplier', - content: , + content: , }, // { // id: '2', @@ -23,8 +28,20 @@ const LogisticStockTabs = () => { ]; return ( -
- +
+
); }; diff --git a/src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx b/src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx new file mode 100644 index 00000000..a5268b2f --- /dev/null +++ b/src/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; +import { ColumnDef } from '@tanstack/react-table'; + +const PurchasePerSupplierSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+ +
+ +
+ + ); +}; + +export default PurchasePerSupplierSkeleton; diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index e1659470..4176e8ba 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -1,11 +1,9 @@ -import { useState, useMemo, useCallback } from 'react'; -import { ChangeEventHandler } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import Card from '@/components/Card'; -import SelectInput, { - useSelect, - OptionType, -} from '@/components/input/SelectInput'; +import { useSelect, OptionType } from '@/components/input/SelectInput'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import DateInput from '@/components/input/DateInput'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; @@ -14,24 +12,30 @@ import { ProductCategoryApi } from '@/services/api/master-data'; import { LogisticApi } from '@/services/api/report/logistic-stock'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; import { LogisticPurchasePerSupplierReport, LogisticPurchasePerSupplierSummary, } from '@/types/api/report/logistic-stock'; import { isResponseSuccess } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import Pagination from '@/components/Pagination'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; +import Modal from '@/components/Modal'; +import { useModal } from '@/components/Modal'; import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX'; +import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; +import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; -const PurchasesPerSupplierTab = () => { +interface PurchasesPerSupplierTabProps { + tabId: string; +} + +const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); @@ -39,31 +43,14 @@ const PurchasesPerSupplierTab = () => { // ===== PAGINATION STATE ===== const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); + const [pageSize] = useState(10); // ===== SUBMISSION STATE ===== const [isSubmitted, setIsSubmitted] = useState(false); - // ===== TABLE FILTER STATE ===== - const { state: tableFilterState, updateFilter } = useTableFilter({ - initial: { - area_id: [] as string[], - supplier_id: [] as string[], - product_id: [] as string[], - product_category_id: [] as string[], - received_date: '', - po_date: '', - start_date: '', - end_date: '', - sort_by: '', - filter_by: 'received_date', - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); + const filterModal = useModal(); + // ===== OPTIONS (Declare before filter state) ===== const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, 'id', @@ -100,117 +87,232 @@ const PurchasesPerSupplierTab = () => { [] ); - const areaChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'area_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); + // ===== APPLIED FILTER STATE (Yang sudah di-apply) ===== + const [appliedFilterArea, setAppliedFilterArea] = useState< + typeof areaOptions + >([]); + const [appliedFilterSupplier, setAppliedFilterSupplier] = useState< + typeof supplierOptions + >([]); + const [appliedFilterProduct, setAppliedFilterProduct] = useState< + typeof productOptions + >([]); + const [appliedFilterProductCategory, setAppliedFilterProductCategory] = + useState([]); + const [appliedFilterByType, setAppliedFilterByType] = useState< + (typeof dataTypeOptions)[0] | null + >(null); + const [appliedFilterSortBy, setAppliedFilterSortBy] = useState< + (typeof sortByOptions)[0] | null + >(null); + const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); + const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); - const supplierChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'supplier_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] + // ===== PENDING FILTER STATE (Yang ada di modal, belum di-apply) ===== + const [filterArea, setFilterArea] = useState([]); + const [filterSupplier, setFilterSupplier] = useState( + [] ); + const [filterProduct, setFilterProduct] = useState([]); + const [filterProductCategory, setFilterProductCategory] = useState< + typeof productCategoryOptions + >([]); + const [filterByType, setFilterByType] = useState< + (typeof dataTypeOptions)[0] | null + >(null); + const [filterSortBy, setFilterSortBy] = useState< + (typeof sortByOptions)[0] | null + >(null); + const [filterStartDate, setFilterStartDate] = useState(''); + const [filterEndDate, setFilterEndDate] = useState(''); - const productChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'product_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); + // ===== FILTER HANDLERS ===== + const handleFilterModalOpen = useCallback(() => { + setFilterArea(appliedFilterArea); + setFilterSupplier(appliedFilterSupplier); + setFilterProduct(appliedFilterProduct); + setFilterProductCategory(appliedFilterProductCategory); + setFilterByType(appliedFilterByType); + setFilterSortBy(appliedFilterSortBy); + setFilterStartDate(appliedFilterStartDate); + setFilterEndDate(appliedFilterEndDate); + filterModal.openModal(); + }, [ + filterModal, + appliedFilterArea, + appliedFilterSupplier, + appliedFilterProduct, + appliedFilterProductCategory, + appliedFilterByType, + appliedFilterSortBy, + appliedFilterStartDate, + appliedFilterEndDate, + ]); - const productCategoryChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'product_category_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const dataTypeChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - const filterValue = - (newVal?.value as 'received_date' | 'po_date') || 'received_date'; - updateFilter('filter_by', filterValue); - updateFilter('received_date', ''); - updateFilter('po_date', ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const sortByHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - const sortValue = (newVal?.value as 'ASC' | 'DESC') || 'ASC'; - updateFilter('sort_by', sortValue); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const startDateChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - updateFilter('start_date', val || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const endDateChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - updateFilter('end_date', val || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const resetFilters = useCallback(() => { - updateFilter('area_id', []); - updateFilter('supplier_id', []); - updateFilter('product_id', []); - updateFilter('product_category_id', []); - updateFilter('received_date', ''); - updateFilter('po_date', ''); - updateFilter('start_date', ''); - updateFilter('end_date', ''); - updateFilter('sort_by', ''); - updateFilter('filter_by', 'received_date'); + const handleResetFilters = useCallback(() => { setIsSubmitted(false); - }, [updateFilter]); + setFilterArea([]); + setFilterSupplier([]); + setFilterProduct([]); + setFilterProductCategory([]); + setFilterByType(null); + setFilterSortBy(null); + setFilterStartDate(''); + setFilterEndDate(''); + setAppliedFilterArea([]); + setAppliedFilterSupplier([]); + setAppliedFilterProduct([]); + setAppliedFilterProductCategory([]); + setAppliedFilterByType(null); + setAppliedFilterSortBy(null); + setAppliedFilterStartDate(''); + setAppliedFilterEndDate(''); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, [dateErrorShown]); - const handleSubmit = useCallback(() => { + const handleApplyFilters = useCallback(() => { + setAppliedFilterArea(filterArea); + setAppliedFilterSupplier(filterSupplier); + setAppliedFilterProduct(filterProduct); + setAppliedFilterProductCategory(filterProductCategory); + setAppliedFilterByType(filterByType); + setAppliedFilterSortBy(filterSortBy); + setAppliedFilterStartDate(filterStartDate); + setAppliedFilterEndDate(filterEndDate); setIsSubmitted(true); setCurrentPage(1); - }, []); + filterModal.closeModal(); + }, [ + filterModal, + filterArea, + filterSupplier, + filterProduct, + filterProductCategory, + filterByType, + filterSortBy, + filterStartDate, + filterEndDate, + ]); + + const handleStartDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterStartDate(value); + + if (value && filterEndDate) { + const startDate = new Date(value); + const endDateObj = new Date(filterEndDate); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }, + [filterEndDate, dateErrorShown] + ); + + const handleEndDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterEndDate(value); + + if (value && filterStartDate) { + const startDateObj = new Date(filterStartDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + [filterStartDate, dateErrorShown] + ); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + // Date filter (start_date + end_date = 1 filter) + if (appliedFilterStartDate || appliedFilterEndDate) { + count += 1; + } + + // Area filter + if (appliedFilterArea.length > 0) { + count += 1; + } + + // Supplier filter + if (appliedFilterSupplier.length > 0) { + count += 1; + } + + // Product filter + if (appliedFilterProduct.length > 0) { + count += 1; + } + + // Product category filter + if (appliedFilterProductCategory.length > 0) { + count += 1; + } + + // Filter by type filter + if (appliedFilterByType) { + count += 1; + } + + // Sort by filter + if (appliedFilterSortBy) { + count += 1; + } + + return count; + }, [ + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterArea, + appliedFilterSupplier, + appliedFilterProduct, + appliedFilterProductCategory, + appliedFilterByType, + appliedFilterSortBy, + ]); + + const hasFilters = activeFiltersCount > 0; // ===== DATA FETCHING ===== const { data: purchasePerSupplier, isLoading } = useSWR( @@ -218,33 +320,38 @@ const PurchasesPerSupplierTab = () => { ? () => { const params = { area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') + appliedFilterArea.length > 0 + ? appliedFilterArea.map((v) => String(v.value)).join(',') : undefined, supplier_id: - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id.join(',') + appliedFilterSupplier.length > 0 + ? appliedFilterSupplier.map((v) => String(v.value)).join(',') : undefined, product_id: - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id.join(',') + appliedFilterProduct.length > 0 + ? appliedFilterProduct.map((v) => String(v.value)).join(',') : undefined, product_category_id: - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id.join(',') + appliedFilterProductCategory.length > 0 + ? appliedFilterProductCategory + .map((v) => String(v.value)) + .join(',') : undefined, received_date: - tableFilterState.filter_by === 'received_date' - ? tableFilterState.start_date || undefined + appliedFilterByType?.value === 'received_date' + ? appliedFilterStartDate || undefined : undefined, po_date: - tableFilterState.filter_by === 'po_date' - ? tableFilterState.start_date || undefined + appliedFilterByType?.value === 'po_date' + ? appliedFilterStartDate || undefined : undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - sort_by: tableFilterState.sort_by || undefined, - filter_by: tableFilterState.filter_by || undefined, + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, + sort_by: + (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined, + filter_by: + (appliedFilterByType?.value as 'received_date' | 'po_date') || + undefined, page: currentPage, limit: pageSize, }; @@ -289,33 +396,35 @@ const PurchasesPerSupplierTab = () => { > => { const params = { area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') + appliedFilterArea.length > 0 + ? appliedFilterArea.map((v) => String(v.value)).join(',') : undefined, supplier_id: - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id.join(',') + appliedFilterSupplier.length > 0 + ? appliedFilterSupplier.map((v) => String(v.value)).join(',') : undefined, product_id: - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id.join(',') + appliedFilterProduct.length > 0 + ? appliedFilterProduct.map((v) => String(v.value)).join(',') : undefined, product_category_id: - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id.join(',') + appliedFilterProductCategory.length > 0 + ? appliedFilterProductCategory.map((v) => String(v.value)).join(',') : undefined, received_date: - tableFilterState.filter_by === 'received_date' - ? tableFilterState.start_date || undefined + appliedFilterByType?.value === 'received_date' + ? appliedFilterStartDate || undefined : undefined, po_date: - tableFilterState.filter_by === 'po_date' - ? tableFilterState.start_date || undefined + appliedFilterByType?.value === 'po_date' + ? appliedFilterStartDate || undefined : undefined, - start_date: tableFilterState.start_date || undefined, - end_date: tableFilterState.end_date || undefined, - sort_by: tableFilterState.sort_by || undefined, - filter_by: tableFilterState.filter_by || undefined, + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, + sort_by: (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined, + filter_by: + (appliedFilterByType?.value as 'received_date' | 'po_date') || + undefined, limit: 100, page: 1, }; @@ -338,7 +447,16 @@ const PurchasesPerSupplierTab = () => { return isResponseSuccess(response) ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) : null; - }, [tableFilterState]); + }, [ + appliedFilterArea, + appliedFilterSupplier, + appliedFilterProduct, + appliedFilterProductCategory, + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterByType, + appliedFilterSortBy, + ]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -379,48 +497,26 @@ const PurchasesPerSupplierTab = () => { } const areaName = - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id - .map( - (id) => - areaOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Area' + appliedFilterArea.length > 0 + ? appliedFilterArea.map((c) => c.label).join(', ') || 'Semua Area' : 'Semua Area'; const supplierName = - tableFilterState.supplier_id.length > 0 - ? tableFilterState.supplier_id - .map( - (id) => - supplierOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Supplier' + appliedFilterSupplier.length > 0 + ? appliedFilterSupplier.map((c) => c.label).join(', ') || + 'Semua Supplier' : 'Semua Supplier'; const productName = - tableFilterState.product_id.length > 0 - ? tableFilterState.product_id - .map( - (id) => - productOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Produk' + appliedFilterProduct.length > 0 + ? appliedFilterProduct.map((c) => c.label).join(', ') || + 'Semua Produk' : 'Semua Produk'; const productCategoryName = - tableFilterState.product_category_id.length > 0 - ? tableFilterState.product_category_id - .map( - (id) => - productCategoryOptions.find((opt) => opt.value === Number(id)) - ?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Kategori Produk' + appliedFilterProductCategory.length > 0 + ? appliedFilterProductCategory.map((c) => c.label).join(', ') || + 'Semua Kategori Produk' : 'Semua Kategori Produk'; const exportParams = { @@ -428,9 +524,11 @@ const PurchasesPerSupplierTab = () => { supplier_name: supplierName, product_name: productName, product_category_name: productCategoryName, - filter_by: tableFilterState.filter_by || 'received_date', - start_date: tableFilterState.start_date || '', - end_date: tableFilterState.end_date || '', + filter_by: + (appliedFilterByType?.value as 'received_date' | 'po_date') || + undefined, + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, }; await generatePurchasesPerSupplierPDF({ @@ -445,33 +543,101 @@ const PurchasesPerSupplierTab = () => { } }, [ logisticPurchasePerSupplierExport, - tableFilterState, - areaOptions, - supplierOptions, - productOptions, - productCategoryOptions, + appliedFilterArea, + appliedFilterSupplier, + appliedFilterProduct, + appliedFilterProductCategory, + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterByType, ]); - // ===== PAGINATION HANDLERS ===== - const handlePageChange = (page: number) => { - setCurrentPage(page); - }; + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useLogisticStockTabStore( + (state) => state.setTabActions + ); + const clearTabActions = useLogisticStockTabStore( + (state) => state.clearTabActions + ); - const handleRowChange = (pageSize: number) => { - setPageSize(pageSize); - }; + useEffect(() => { + setTabActions( + tabId, +
+ - const handleNextPage = () => { - if (meta && currentPage < meta.total_pages) { - setCurrentPage(currentPage + 1); - } - }; + + + Export +
+ +
+ + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
+
+ ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + filterModal.open, + setTabActions, + ]); - const handlePrevPage = () => { - if (currentPage > 1) { - setCurrentPage(currentPage - 1); - } - }; + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); const getTableColumns = ( summary: LogisticPurchasePerSupplierSummary @@ -485,11 +651,11 @@ const PurchasesPerSupplierTab = () => { cell: (props) => props.row.index + 1, footer: () =>
Total
, }, - { id: 'received_date', header: 'Tanggal Terima', accessorKey: 'receive_date', + enableSorting: false, cell: (props) => { const value = props.row.original.receive_date; return formatDate(value, 'DD MMM YYYY'); @@ -499,6 +665,7 @@ const PurchasesPerSupplierTab = () => { id: 'po_date', header: 'Tanggal PO', accessorKey: 'po_date', + enableSorting: false, cell: (props) => { const value = props.row.original.po_date; return formatDate(value, 'DD MMM YYYY'); @@ -508,6 +675,7 @@ const PurchasesPerSupplierTab = () => { id: 'po_number', header: 'No. Referensi', accessorKey: 'po_number', + enableSorting: false, cell: (props) => { const value = props.row.original.po_number; return value || '-'; @@ -517,6 +685,7 @@ const PurchasesPerSupplierTab = () => { id: 'product_name', header: 'Nama Produk', accessorKey: 'product.name', + enableSorting: false, cell: (props) => { const product = props.row.original.product; return product?.name || '-'; @@ -526,6 +695,7 @@ const PurchasesPerSupplierTab = () => { id: 'destination_warehouse', header: 'Tujuan', accessorKey: 'warehouse.name', + enableSorting: false, cell: (props) => { const warehouse = props.row.original.warehouse; return warehouse?.name || '-'; @@ -535,6 +705,7 @@ const PurchasesPerSupplierTab = () => { id: 'qty', header: 'QTY', accessorKey: 'qty', + enableSorting: false, cell: (props) => { const value = props.row.original.qty; return
{formatNumber(value)}
; @@ -549,6 +720,7 @@ const PurchasesPerSupplierTab = () => { id: 'price', header: 'Harga Beli (Rp)', accessorKey: 'unit_price', + enableSorting: false, cell: (props) => { const value = props.row.original.unit_price; return
{formatCurrency(value)}
; @@ -563,6 +735,7 @@ const PurchasesPerSupplierTab = () => { id: 'purchase_amount', header: 'Value Harga Beli (Rp)', accessorKey: 'purchase_value', + enableSorting: false, cell: (props) => { const value = props.row.original.purchase_value; return
{formatCurrency(value)}
; @@ -577,6 +750,7 @@ const PurchasesPerSupplierTab = () => { id: 'transport', header: 'Transport (Rp)', accessorKey: 'transport_unit_price', + enableSorting: false, cell: (props) => { const value = props.row.original.transport_unit_price; return
{formatCurrency(value)}
; @@ -591,6 +765,7 @@ const PurchasesPerSupplierTab = () => { id: 'value_transport', header: 'Value Transport (Rp)', accessorKey: 'transport_value', + enableSorting: false, cell: (props) => { const value = props.row.original.transport_value; return
{formatCurrency(value)}
; @@ -605,6 +780,7 @@ const PurchasesPerSupplierTab = () => { id: 'total', header: 'Jumlah (Rp)', accessorKey: 'total_amount', + enableSorting: false, cell: (props) => { const value = props.row.original.total_amount; return
{formatCurrency(value)}
; @@ -619,6 +795,7 @@ const PurchasesPerSupplierTab = () => { id: 'expedition_vendor_name', header: 'Ekspedisi', accessorKey: 'expedition', + enableSorting: false, cell: (props) => { const value = props.row.original.expedition; return value || '-'; @@ -628,6 +805,7 @@ const PurchasesPerSupplierTab = () => { id: 'travel_number', header: 'Surat Jalan', accessorKey: 'delivery_number', + enableSorting: false, cell: (props) => { const value = props.row.original.delivery_number; return value || '-'; @@ -638,156 +816,50 @@ const PurchasesPerSupplierTab = () => { }; return ( -
- -
- - (tableFilterState.area_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={areaChangeHandler} - isLoading={isLoadingAreas} - isClearable - /> - - (tableFilterState.supplier_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={supplierChangeHandler} - isLoading={isLoadingSuppliers} - isClearable - /> - - (tableFilterState.product_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={productChangeHandler} - isLoading={isLoadingProducts} - isClearable - /> -
-
- - (tableFilterState.product_category_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={productCategoryChangeHandler} - isLoading={isLoadingProductCategories} - isClearable - /> -
- option.value === tableFilterState.filter_by - ) || null - } - onChange={dataTypeChangeHandler} - isLoading={false} - isClearable={false} - /> - option.value === tableFilterState.sort_by - ) || null - } - onChange={sortByHandler} - isLoading={false} - isClearable={false} - /> -
-
- - -
-
-
- - - - Export - - - } - align='end' - > - - - - - -
- + <> +
{!isSubmitted ? ( -
- Silakan pilih filter dan klik tombol Submit untuk menampilkan data. -
+ + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> ) : isLoading ? ( -
- -
+ + } + title='Memuat Data Pembelian Per Supplier' + subtitle='Silakan tunggu sebentar...' + /> ) : data.length === 0 ? ( -
- Tidak ada data yang dapat ditampilkan... -
+ + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> ) : ( data.map((supplierReport) => { const summary = supplierReport.summary || { @@ -808,15 +880,17 @@ const PurchasesPerSupplierTab = () => { title={supplierReport.supplier.name} subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`} className={{ - wrapper: 'w-full rounded-2xl', + wrapper: 'w-full rounded-lg border-none', body: 'p-0', title: - 'py-1.5 px-3 bg-primary text-white text-lg font-normal', + 'px-2 py-1.5 font-normal text-sm bg-primary text-white', subtitle: - 'px-3 pb-1 bg-primary text-white text-sm font-normal', + 'px-2 pb-1.5 bg-primary text-white text-xs font-normal', + collapsible: 'rounded-lg', }} variant='bordered' collapsible={true} + defaultCollapsed={true} >
{ renderFooter={supplierReport.rows.length > 0} className={{ containerClassName: 'w-full mb-0!', - tableWrapperClassName: 'overflow-x-auto', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: @@ -846,22 +921,161 @@ const PurchasesPerSupplierTab = () => { ); }) )} - - {meta && data.length > 0 && ( -
- + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+ {/* Date Filter */} +
+ +
+ +
+ +
+
+ + {/* Area Filter */} + { + setFilterArea(Array.isArray(val) ? val : val ? [val] : []); + }} + isLoading={isLoadingAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Supplier Filter */} + { + setFilterSupplier(Array.isArray(val) ? val : val ? [val] : []); + }} + isLoading={isLoadingSuppliers} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Filter */} + { + setFilterProduct(Array.isArray(val) ? val : val ? [val] : []); + }} + isLoading={isLoadingProducts} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Category Filter */} + { + setFilterProductCategory( + Array.isArray(val) ? val : val ? [val] : [] + ); + }} + isLoading={isLoadingProductCategories} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Filter By Type */} + { + if (val && !Array.isArray(val)) { + setFilterByType(val); + } + }} + className={{ wrapper: 'w-full' }} + /> + + {/* Sort By */} + { + if (val && !Array.isArray(val)) { + setFilterSortBy(val); + } + }} + className={{ wrapper: 'w-full' }} />
- )} -
+ + {/* Modal Footer */} +
+ + +
+ + ); }; diff --git a/src/stores/logistic-stock-tab/logistic-stock-tab.store.ts b/src/stores/logistic-stock-tab/logistic-stock-tab.store.ts new file mode 100644 index 00000000..f9e142b1 --- /dev/null +++ b/src/stores/logistic-stock-tab/logistic-stock-tab.store.ts @@ -0,0 +1,51 @@ +'use client'; + +import { ReactNode } from 'react'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +export type LogisticStockTabActionsSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const useLogisticStockTabStore = create()( + devtools( + (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set( + (state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + }), + false, + 'setTabActions' + ), + + clearTabActions: (tabId) => + set( + (state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }, + false, + 'clearTabActions' + ), + + clearAllTabActions: () => + set({ tabActions: {} }, false, 'clearAllTabActions'), + }), + { + name: 'LogisticStockTabStore', + } + ) +); From ed781da372b1a89c4f07240eff5d3cdd02cf57a2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 16:12:14 +0700 Subject: [PATCH 061/149] refactor(FE): Change Tabs variant from 'lifted' to 'boxed' --- .../pages/report/logistic-stock/LogisticStockTabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index a7b844f3..f06e63dc 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -31,7 +31,7 @@ const LogisticStockTabs = () => {
Date: Wed, 11 Feb 2026 16:17:15 +0700 Subject: [PATCH 062/149] refactor(FE): Make dropdown filters clearable in PurchasesPerSupplierTab --- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 4176e8ba..023df222 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -1035,11 +1035,12 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { options={dataTypeOptions} value={filterByType} onChange={(val) => { - if (val && !Array.isArray(val)) { + if (!Array.isArray(val)) { setFilterByType(val); } }} className={{ wrapper: 'w-full' }} + isClearable={true} /> {/* Sort By */} @@ -1049,11 +1050,12 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { options={sortByOptions} value={filterSortBy} onChange={(val) => { - if (val && !Array.isArray(val)) { + if (!Array.isArray(val)) { setFilterSortBy(val); } }} className={{ wrapper: 'w-full' }} + isClearable={true} /> From 52d58d0921ff351c3a6d69f3fa444f053c9ee21c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 11 Feb 2026 16:44:10 +0700 Subject: [PATCH 063/149] refactor(FE): Refactor PurchasesPerSupplierTab to use Formik for filters --- .../filter/PurchasesPerSupplierFilter.ts | 89 +++ .../tab/PurchasesPerSupplierTab.tsx | 755 ++++++++---------- 2 files changed, 439 insertions(+), 405 deletions(-) create mode 100644 src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts diff --git a/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts new file mode 100644 index 00000000..70a01615 --- /dev/null +++ b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts @@ -0,0 +1,89 @@ +import { OptionType } from '@/components/input/SelectInput'; +import * as yup from 'yup'; + +export type PurchasesPerSupplierFilterType = { + start_date: string | null | undefined; + end_date: string | null | undefined; + area_ids: OptionType[] | null | undefined; + supplier_ids: OptionType[] | null | undefined; + product_ids: OptionType[] | null | undefined; + product_category_ids: OptionType[] | null | undefined; + filter_by: OptionType | null | undefined; + sort_by: OptionType | null | undefined; +}; + +export const PurchasesPerSupplierFilterSchema: yup.ObjectSchema = + yup.object({ + start_date: yup.string().optional().nullable(), + end_date: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + area_ids: yup + .array() + .of( + yup.object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + ) + .optional() + .nullable(), + supplier_ids: yup + .array() + .of( + yup.object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + ) + .optional() + .nullable(), + product_ids: yup + .array() + .of( + yup.object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + ) + .optional() + .nullable(), + product_category_ids: yup + .array() + .of( + yup.object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + ) + .optional() + .nullable(), + filter_by: yup + .object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + .optional() + .nullable(), + sort_by: yup + .object({ + value: yup.mixed().required(), + label: yup.string().required(), + }) + .optional() + .nullable(), + }); + +export type PurchasesPerSupplierFilterValues = yup.InferType< + typeof PurchasesPerSupplierFilterSchema +>; diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 023df222..4a070b56 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -1,40 +1,55 @@ -import { useState, useMemo, useCallback, useEffect } from 'react'; -import useSWR from 'swr'; +import Button from '@/components/Button'; import Card from '@/components/Card'; -import { useSelect, OptionType } from '@/components/input/SelectInput'; -import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; -import SelectInputRadio from '@/components/input/SelectInputRadio'; +import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; +import { OptionType, useSelect } from '@/components/input/SelectInput'; +import Menu from '@/components/menu/Menu'; +import MenuItem from '@/components/menu/MenuItem'; +import Modal, { useModal } from '@/components/Modal'; +import Table from '@/components/Table'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { AreaApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data'; import { ProductApi } from '@/services/api/master-data'; import { ProductCategoryApi } from '@/services/api/master-data'; import { LogisticApi } from '@/services/api/report/logistic-stock'; -import Table from '@/components/Table'; -import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; import { LogisticPurchasePerSupplierReport, LogisticPurchasePerSupplierSummary, } from '@/types/api/report/logistic-stock'; -import { isResponseSuccess } from '@/lib/api-helper'; -import Button from '@/components/Button'; -import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; -import Modal from '@/components/Modal'; -import { useModal } from '@/components/Modal'; -import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; import { generatePurchasesPerSupplierExcel } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportXLSX'; -import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; -import toast from 'react-hot-toast'; +import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF'; import { Icon } from '@iconify/react'; +import { ColumnDef } from '@tanstack/react-table'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import toast from 'react-hot-toast'; +import useSWR from 'swr'; +import { useFormik } from 'formik'; +import { + PurchasesPerSupplierFilterSchema, + PurchasesPerSupplierFilterType, +} from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; +import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; interface PurchasesPerSupplierTabProps { tabId: string; } +interface FilterParams { + area_ids?: string; + supplier_ids?: string; + product_ids?: string; + product_category_ids?: string; + start_date?: string; + end_date?: string; + sort_by?: string; + filter_by?: string; +} + const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); @@ -46,11 +61,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { const [pageSize] = useState(10); // ===== SUBMISSION STATE ===== + const [filterParams, setFilterParams] = useState({}); const [isSubmitted, setIsSubmitted] = useState(false); + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); const filterModal = useModal(); - // ===== OPTIONS (Declare before filter state) ===== + // ===== OPTIONS ===== const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( AreaApi.basePath, 'id', @@ -87,127 +105,66 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { [] ); - // ===== APPLIED FILTER STATE (Yang sudah di-apply) ===== - const [appliedFilterArea, setAppliedFilterArea] = useState< - typeof areaOptions - >([]); - const [appliedFilterSupplier, setAppliedFilterSupplier] = useState< - typeof supplierOptions - >([]); - const [appliedFilterProduct, setAppliedFilterProduct] = useState< - typeof productOptions - >([]); - const [appliedFilterProductCategory, setAppliedFilterProductCategory] = - useState([]); - const [appliedFilterByType, setAppliedFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const [appliedFilterSortBy, setAppliedFilterSortBy] = useState< - (typeof sortByOptions)[0] | null - >(null); - const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); - const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); - const [dateErrorShown, setDateErrorShown] = useState(false); - const [hasDateError, setHasDateError] = useState(false); - - // ===== PENDING FILTER STATE (Yang ada di modal, belum di-apply) ===== - const [filterArea, setFilterArea] = useState([]); - const [filterSupplier, setFilterSupplier] = useState( - [] - ); - const [filterProduct, setFilterProduct] = useState([]); - const [filterProductCategory, setFilterProductCategory] = useState< - typeof productCategoryOptions - >([]); - const [filterByType, setFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const [filterSortBy, setFilterSortBy] = useState< - (typeof sortByOptions)[0] | null - >(null); - const [filterStartDate, setFilterStartDate] = useState(''); - const [filterEndDate, setFilterEndDate] = useState(''); - - // ===== FILTER HANDLERS ===== - const handleFilterModalOpen = useCallback(() => { - setFilterArea(appliedFilterArea); - setFilterSupplier(appliedFilterSupplier); - setFilterProduct(appliedFilterProduct); - setFilterProductCategory(appliedFilterProductCategory); - setFilterByType(appliedFilterByType); - setFilterSortBy(appliedFilterSortBy); - setFilterStartDate(appliedFilterStartDate); - setFilterEndDate(appliedFilterEndDate); + const handleFilterModalOpen = () => { filterModal.openModal(); - }, [ - filterModal, - appliedFilterArea, - appliedFilterSupplier, - appliedFilterProduct, - appliedFilterProductCategory, - appliedFilterByType, - appliedFilterSortBy, - appliedFilterStartDate, - appliedFilterEndDate, - ]); + }; - const handleResetFilters = useCallback(() => { - setIsSubmitted(false); - setFilterArea([]); - setFilterSupplier([]); - setFilterProduct([]); - setFilterProductCategory([]); - setFilterByType(null); - setFilterSortBy(null); - setFilterStartDate(''); - setFilterEndDate(''); - setAppliedFilterArea([]); - setAppliedFilterSupplier([]); - setAppliedFilterProduct([]); - setAppliedFilterProductCategory([]); - setAppliedFilterByType(null); - setAppliedFilterSortBy(null); - setAppliedFilterStartDate(''); - setAppliedFilterEndDate(''); - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }, [dateErrorShown]); - - const handleApplyFilters = useCallback(() => { - setAppliedFilterArea(filterArea); - setAppliedFilterSupplier(filterSupplier); - setAppliedFilterProduct(filterProduct); - setAppliedFilterProductCategory(filterProductCategory); - setAppliedFilterByType(filterByType); - setAppliedFilterSortBy(filterSortBy); - setAppliedFilterStartDate(filterStartDate); - setAppliedFilterEndDate(filterEndDate); - setIsSubmitted(true); - setCurrentPage(1); - filterModal.closeModal(); - }, [ - filterModal, - filterArea, - filterSupplier, - filterProduct, - filterProductCategory, - filterByType, - filterSortBy, - filterStartDate, - filterEndDate, - ]); + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + start_date: null, + end_date: null, + area_ids: null, + supplier_ids: null, + product_ids: null, + product_category_ids: null, + filter_by: null, + sort_by: null, + }, + validationSchema: PurchasesPerSupplierFilterSchema, + onSubmit: (values) => { + setFilterParams({ + start_date: values.start_date?.toString() || undefined, + end_date: values.end_date?.toString() || undefined, + area_ids: + values.area_ids?.map((v) => String(v.value)).join(',') || undefined, + supplier_ids: + values.supplier_ids?.map((v) => String(v.value)).join(',') || + undefined, + product_ids: + values.product_ids?.map((v) => String(v.value)).join(',') || + undefined, + product_category_ids: + values.product_category_ids?.map((v) => String(v.value)).join(',') || + undefined, + filter_by: values.filter_by?.value?.toString() || undefined, + sort_by: values.sort_by?.value?.toString() || undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setCurrentPage(1); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setCurrentPage(1); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + }); + // ===== DATE CHANGE HANDLERS ===== const handleStartDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterStartDate(value); + formik.setFieldValue('start_date', value || null); - if (value && filterEndDate) { + if (value && formik.values.end_date) { const startDate = new Date(value); - const endDateObj = new Date(filterEndDate); + const endDateObj = new Date(formik.values.end_date); if (endDateObj < startDate) { setHasDateError(true); @@ -228,16 +185,16 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setHasDateError(false); } }, - [filterEndDate, dateErrorShown] + [formik, dateErrorShown] ); const handleEndDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterEndDate(value); + formik.setFieldValue('end_date', value || null); - if (value && filterStartDate) { - const startDateObj = new Date(filterStartDate); + if (value && formik.values.start_date) { + const startDateObj = new Date(formik.values.start_date); const endDate = new Date(value); if (endDate < startDateObj) { @@ -258,59 +215,43 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setDateErrorShown(false); } }, - [filterStartDate, dateErrorShown] + [formik, dateErrorShown] ); // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; - // Date filter (start_date + end_date = 1 filter) - if (appliedFilterStartDate || appliedFilterEndDate) { + if (filterParams.start_date || filterParams.end_date) { count += 1; } - // Area filter - if (appliedFilterArea.length > 0) { + if (filterParams.area_ids) { count += 1; } - // Supplier filter - if (appliedFilterSupplier.length > 0) { + if (filterParams.supplier_ids) { count += 1; } - // Product filter - if (appliedFilterProduct.length > 0) { + if (filterParams.product_ids) { count += 1; } - // Product category filter - if (appliedFilterProductCategory.length > 0) { + if (filterParams.product_category_ids) { count += 1; } - // Filter by type filter - if (appliedFilterByType) { + if (filterParams.filter_by) { count += 1; } - // Sort by filter - if (appliedFilterSortBy) { + if (filterParams.sort_by) { count += 1; } return count; - }, [ - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterArea, - appliedFilterSupplier, - appliedFilterProduct, - appliedFilterProductCategory, - appliedFilterByType, - appliedFilterSortBy, - ]); + }, [filterParams]); const hasFilters = activeFiltersCount > 0; @@ -319,39 +260,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { isSubmitted ? () => { const params = { - area_id: - appliedFilterArea.length > 0 - ? appliedFilterArea.map((v) => String(v.value)).join(',') - : undefined, - supplier_id: - appliedFilterSupplier.length > 0 - ? appliedFilterSupplier.map((v) => String(v.value)).join(',') - : undefined, - product_id: - appliedFilterProduct.length > 0 - ? appliedFilterProduct.map((v) => String(v.value)).join(',') - : undefined, - product_category_id: - appliedFilterProductCategory.length > 0 - ? appliedFilterProductCategory - .map((v) => String(v.value)) - .join(',') - : undefined, - received_date: - appliedFilterByType?.value === 'received_date' - ? appliedFilterStartDate || undefined - : undefined, - po_date: - appliedFilterByType?.value === 'po_date' - ? appliedFilterStartDate || undefined - : undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, - sort_by: - (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined, - filter_by: - (appliedFilterByType?.value as 'received_date' | 'po_date') || - undefined, + area_ids: filterParams.area_ids, + supplier_ids: filterParams.supplier_ids, + product_ids: filterParams.product_ids, + product_category_ids: filterParams.product_category_ids, + start_date: filterParams.start_date, + end_date: filterParams.end_date, + sort_by: filterParams.sort_by, + filter_by: filterParams.filter_by, page: currentPage, limit: pageSize, }; @@ -361,12 +277,12 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { : null, ([, params]) => LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_id, - params.supplier_id, - params.product_id, - params.product_category_id, - params.received_date, - params.po_date, + params.area_ids, + params.supplier_ids, + params.product_ids, + params.product_category_ids, + params.filter_by === 'received_date' ? params.start_date : undefined, + params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, params.end_date, params.sort_by, @@ -395,47 +311,25 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { LogisticPurchasePerSupplierReport[] | null > => { const params = { - area_id: - appliedFilterArea.length > 0 - ? appliedFilterArea.map((v) => String(v.value)).join(',') - : undefined, - supplier_id: - appliedFilterSupplier.length > 0 - ? appliedFilterSupplier.map((v) => String(v.value)).join(',') - : undefined, - product_id: - appliedFilterProduct.length > 0 - ? appliedFilterProduct.map((v) => String(v.value)).join(',') - : undefined, - product_category_id: - appliedFilterProductCategory.length > 0 - ? appliedFilterProductCategory.map((v) => String(v.value)).join(',') - : undefined, - received_date: - appliedFilterByType?.value === 'received_date' - ? appliedFilterStartDate || undefined - : undefined, - po_date: - appliedFilterByType?.value === 'po_date' - ? appliedFilterStartDate || undefined - : undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, - sort_by: (appliedFilterSortBy?.value as 'ASC' | 'DESC') || undefined, - filter_by: - (appliedFilterByType?.value as 'received_date' | 'po_date') || - undefined, + area_ids: filterParams.area_ids, + supplier_ids: filterParams.supplier_ids, + product_ids: filterParams.product_ids, + product_category_ids: filterParams.product_category_ids, + start_date: filterParams.start_date, + end_date: filterParams.end_date, + sort_by: filterParams.sort_by, + filter_by: filterParams.filter_by, limit: 100, page: 1, }; const response = await LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_id, - params.supplier_id, - params.product_id, - params.product_category_id, - params.received_date, - params.po_date, + params.area_ids, + params.supplier_ids, + params.product_ids, + params.product_category_ids, + params.filter_by === 'received_date' ? params.start_date : undefined, + params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, params.end_date, params.sort_by, @@ -447,16 +341,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { return isResponseSuccess(response) ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) : null; - }, [ - appliedFilterArea, - appliedFilterSupplier, - appliedFilterProduct, - appliedFilterProductCategory, - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterByType, - appliedFilterSortBy, - ]); + }, [filterParams]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -496,39 +381,52 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { return; } - const areaName = - appliedFilterArea.length > 0 - ? appliedFilterArea.map((c) => c.label).join(', ') || 'Semua Area' - : 'Semua Area'; + const areaName = filterParams.area_ids + ? areaOptions + .filter((opt) => + filterParams.area_ids?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Area' + : 'Semua Area'; - const supplierName = - appliedFilterSupplier.length > 0 - ? appliedFilterSupplier.map((c) => c.label).join(', ') || - 'Semua Supplier' - : 'Semua Supplier'; + const supplierName = filterParams.supplier_ids + ? supplierOptions + .filter((opt) => + filterParams.supplier_ids?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Supplier' + : 'Semua Supplier'; - const productName = - appliedFilterProduct.length > 0 - ? appliedFilterProduct.map((c) => c.label).join(', ') || - 'Semua Produk' - : 'Semua Produk'; + const productName = filterParams.product_ids + ? productOptions + .filter((opt) => + filterParams.product_ids?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Produk' + : 'Semua Produk'; - const productCategoryName = - appliedFilterProductCategory.length > 0 - ? appliedFilterProductCategory.map((c) => c.label).join(', ') || - 'Semua Kategori Produk' - : 'Semua Kategori Produk'; + const productCategoryName = filterParams.product_category_ids + ? productCategoryOptions + .filter((opt) => + filterParams.product_category_ids + ?.split(',') + .includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Kategori Produk' + : 'Semua Kategori Produk'; const exportParams = { area_name: areaName, supplier_name: supplierName, product_name: productName, product_category_name: productCategoryName, - filter_by: - (appliedFilterByType?.value as 'received_date' | 'po_date') || - undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, + filter_by: filterParams.filter_by, + start_date: filterParams.start_date, + end_date: filterParams.end_date, }; await generatePurchasesPerSupplierPDF({ @@ -543,13 +441,11 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { } }, [ logisticPurchasePerSupplierExport, - appliedFilterArea, - appliedFilterSupplier, - appliedFilterProduct, - appliedFilterProductCategory, - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterByType, + filterParams, + areaOptions, + supplierOptions, + productOptions, + productCategoryOptions, ]); // ===== REGISTER TAB ACTIONS TO STORE ===== @@ -624,6 +520,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { ); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ tabId, hasFilters, @@ -633,6 +530,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setTabActions, ]); + // Cleanup on unmount useEffect(() => { return () => { clearTabActions(tabId); @@ -945,137 +843,184 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { -
- {/* Date Filter */} -
- -
- -
- +
+
+ {/* Date Filter */} +
+ +
+ +
+ +
+ + {/* Area Filter */} + { + formik.setFieldValue( + 'area_ids', + Array.isArray(val) ? val : val ? [val] : null + ); + }} + isLoading={isLoadingAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Supplier Filter */} + { + formik.setFieldValue( + 'supplier_ids', + Array.isArray(val) ? val : val ? [val] : null + ); + }} + isLoading={isLoadingSuppliers} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Filter */} + { + formik.setFieldValue( + 'product_ids', + Array.isArray(val) ? val : val ? [val] : null + ); + }} + isLoading={isLoadingProducts} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Product Category Filter */} + { + formik.setFieldValue( + 'product_category_ids', + Array.isArray(val) ? val : val ? [val] : null + ); + }} + isLoading={isLoadingProductCategories} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Filter By Type */} + { + if (!Array.isArray(val)) { + formik.setFieldValue('filter_by', val); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> + + {/* Sort By */} + { + if (!Array.isArray(val)) { + formik.setFieldValue('sort_by', val); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + />
- {/* Area Filter */} - { - setFilterArea(Array.isArray(val) ? val : val ? [val] : []); - }} - isLoading={isLoadingAreas} - isClearable - className={{ wrapper: 'w-full' }} - /> - - {/* Supplier Filter */} - { - setFilterSupplier(Array.isArray(val) ? val : val ? [val] : []); - }} - isLoading={isLoadingSuppliers} - isClearable - className={{ wrapper: 'w-full' }} - /> - - {/* Product Filter */} - { - setFilterProduct(Array.isArray(val) ? val : val ? [val] : []); - }} - isLoading={isLoadingProducts} - isClearable - className={{ wrapper: 'w-full' }} - /> - - {/* Product Category Filter */} - { - setFilterProductCategory( - Array.isArray(val) ? val : val ? [val] : [] - ); - }} - isLoading={isLoadingProductCategories} - isClearable - className={{ wrapper: 'w-full' }} - /> - - {/* Filter By Type */} - { - if (!Array.isArray(val)) { - setFilterByType(val); - } - }} - className={{ wrapper: 'w-full' }} - isClearable={true} - /> - - {/* Sort By */} - { - if (!Array.isArray(val)) { - setFilterSortBy(val); - } - }} - className={{ wrapper: 'w-full' }} - isClearable={true} - /> -
- - {/* Modal Footer */} -
- - -
+ {/* Modal Footer */} +
+ + +
+ ); From 166e95930b8cb892730ed2108b2b758e94efe37d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 09:35:24 +0700 Subject: [PATCH 064/149] refactor(FE): Remove unused imports and cleanup comments --- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 4a070b56..9702d904 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -2,7 +2,7 @@ import Button from '@/components/Button'; import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; -import { OptionType, useSelect } from '@/components/input/SelectInput'; +import { useSelect } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; @@ -520,7 +520,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => {
); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ tabId, hasFilters, @@ -530,7 +529,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { setTabActions, ]); - // Cleanup on unmount useEffect(() => { return () => { clearTabActions(tabId); From 62dd1de150c7745e9f13fd7cc31f9240189a77f3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 09:45:18 +0700 Subject: [PATCH 065/149] refactor(FE): Reset form values on filter modal open and submit --- .../report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 9702d904..09a96b23 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -107,6 +107,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { const handleFilterModalOpen = () => { filterModal.openModal(); + formik.resetForm({ values: formik.values }); }; // ===== FORMIK SETUP ===== @@ -122,7 +123,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { sort_by: null, }, validationSchema: PurchasesPerSupplierFilterSchema, - onSubmit: (values) => { + onSubmit: (values, { resetForm }) => { setFilterParams({ start_date: values.start_date?.toString() || undefined, end_date: values.end_date?.toString() || undefined, @@ -143,6 +144,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { filterModal.closeModal(); setIsSubmitted(true); setCurrentPage(1); + resetForm({ values }); }, onReset: () => { setFilterParams({}); From 28dabcbeb6f40d61e2f9b1c336bdd0bf6f6937a9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 09:52:21 +0700 Subject: [PATCH 066/149] refactor(FE): Refactor filter parameter keys to singular form --- .../tab/PurchasesPerSupplierTab.tsx | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 09a96b23..794c45d6 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -40,10 +40,10 @@ interface PurchasesPerSupplierTabProps { } interface FilterParams { - area_ids?: string; - supplier_ids?: string; - product_ids?: string; - product_category_ids?: string; + area_id?: string; + supplier_id?: string; + product_id?: string; + product_category_id?: string; start_date?: string; end_date?: string; sort_by?: string; @@ -107,7 +107,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { const handleFilterModalOpen = () => { filterModal.openModal(); - formik.resetForm({ values: formik.values }); + formik.validateForm(); }; // ===== FORMIK SETUP ===== @@ -123,19 +123,19 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { sort_by: null, }, validationSchema: PurchasesPerSupplierFilterSchema, - onSubmit: (values, { resetForm }) => { + onSubmit: (values, { setSubmitting }) => { setFilterParams({ start_date: values.start_date?.toString() || undefined, end_date: values.end_date?.toString() || undefined, - area_ids: + area_id: values.area_ids?.map((v) => String(v.value)).join(',') || undefined, - supplier_ids: + supplier_id: values.supplier_ids?.map((v) => String(v.value)).join(',') || undefined, - product_ids: + product_id: values.product_ids?.map((v) => String(v.value)).join(',') || undefined, - product_category_ids: + product_category_id: values.product_category_ids?.map((v) => String(v.value)).join(',') || undefined, filter_by: values.filter_by?.value?.toString() || undefined, @@ -144,7 +144,7 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { filterModal.closeModal(); setIsSubmitted(true); setCurrentPage(1); - resetForm({ values }); + setSubmitting(false); }, onReset: () => { setFilterParams({}); @@ -228,19 +228,19 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { count += 1; } - if (filterParams.area_ids) { + if (filterParams.area_id) { count += 1; } - if (filterParams.supplier_ids) { + if (filterParams.supplier_id) { count += 1; } - if (filterParams.product_ids) { + if (filterParams.product_id) { count += 1; } - if (filterParams.product_category_ids) { + if (filterParams.product_category_id) { count += 1; } @@ -262,10 +262,10 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { isSubmitted ? () => { const params = { - area_ids: filterParams.area_ids, - supplier_ids: filterParams.supplier_ids, - product_ids: filterParams.product_ids, - product_category_ids: filterParams.product_category_ids, + area_id: filterParams.area_id, + supplier_id: filterParams.supplier_id, + product_id: filterParams.product_id, + product_category_id: filterParams.product_category_id, start_date: filterParams.start_date, end_date: filterParams.end_date, sort_by: filterParams.sort_by, @@ -279,10 +279,10 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { : null, ([, params]) => LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_ids, - params.supplier_ids, - params.product_ids, - params.product_category_ids, + params.area_id, + params.supplier_id, + params.product_id, + params.product_category_id, params.filter_by === 'received_date' ? params.start_date : undefined, params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, @@ -313,10 +313,10 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { LogisticPurchasePerSupplierReport[] | null > => { const params = { - area_ids: filterParams.area_ids, - supplier_ids: filterParams.supplier_ids, - product_ids: filterParams.product_ids, - product_category_ids: filterParams.product_category_ids, + area_id: filterParams.area_id, + supplier_id: filterParams.supplier_id, + product_id: filterParams.product_id, + product_category_id: filterParams.product_category_id, start_date: filterParams.start_date, end_date: filterParams.end_date, sort_by: filterParams.sort_by, @@ -326,10 +326,10 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { }; const response = await LogisticApi.getLogisticPurchasePerSupplierReport( - params.area_ids, - params.supplier_ids, - params.product_ids, - params.product_category_ids, + params.area_id, + params.supplier_id, + params.product_id, + params.product_category_id, params.filter_by === 'received_date' ? params.start_date : undefined, params.filter_by === 'po_date' ? params.start_date : undefined, params.start_date, @@ -383,37 +383,37 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { return; } - const areaName = filterParams.area_ids + const areaName = filterParams.area_id ? areaOptions .filter((opt) => - filterParams.area_ids?.split(',').includes(String(opt.value)) + filterParams.area_id?.split(',').includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Area' : 'Semua Area'; - const supplierName = filterParams.supplier_ids + const supplierName = filterParams.supplier_id ? supplierOptions .filter((opt) => - filterParams.supplier_ids?.split(',').includes(String(opt.value)) + filterParams.supplier_id?.split(',').includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Supplier' : 'Semua Supplier'; - const productName = filterParams.product_ids + const productName = filterParams.product_id ? productOptions .filter((opt) => - filterParams.product_ids?.split(',').includes(String(opt.value)) + filterParams.product_id?.split(',').includes(String(opt.value)) ) .map((opt) => opt.label) .join(', ') || 'Semua Produk' : 'Semua Produk'; - const productCategoryName = filterParams.product_category_ids + const productCategoryName = filterParams.product_category_id ? productCategoryOptions .filter((opt) => - filterParams.product_category_ids + filterParams.product_category_id ?.split(',') .includes(String(opt.value)) ) From fd78ca6ac1aa410f0789e0453dd7f5f82f27e263 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 10:28:36 +0700 Subject: [PATCH 067/149] refactor(FE): Refactor CustomerPaymentTab to use Formik for filter management --- .../finance/filter/CustomerPaymentFilter.ts | 31 ++ .../report/finance/tab/CustomerPaymentTab.tsx | 419 ++++++++---------- 2 files changed, 211 insertions(+), 239 deletions(-) create mode 100644 src/components/pages/report/finance/filter/CustomerPaymentFilter.ts diff --git a/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts b/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts new file mode 100644 index 00000000..60359038 --- /dev/null +++ b/src/components/pages/report/finance/filter/CustomerPaymentFilter.ts @@ -0,0 +1,31 @@ +import * as yup from 'yup'; + +export type CustomerPaymentFilterType = { + start_date: string | null; + end_date: string | null; + customer_ids: string | null; + filter_by: string | null; +}; + +export const CustomerPaymentFilterSchema = yup.object({ + start_date: yup.string().optional().nullable(), + end_date: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + customer_ids: yup.string().nullable(), + filter_by: yup.string().nullable(), +}); + +export type CustomerPaymentFilterValues = yup.InferType< + typeof CustomerPaymentFilterSchema +>; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 4e0e3f25..723a1ebf 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -9,7 +9,6 @@ import SelectInputRadio from '@/components/input/SelectInputRadio'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; -// import { UserApi } from '@/services/api/user'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; @@ -22,18 +21,30 @@ import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; -import Modal from '@/components/Modal'; -import { useModal } from '@/components/Modal'; +import Modal, { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; +import { useFormik } from 'formik'; +import { + CustomerPaymentFilterSchema, + CustomerPaymentFilterType, +} from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; +import { OptionType } from '@/components/table/TableRowSizeSelector'; interface CustomerPaymentTabProps { tabId: string; } +interface FilterParams { + customer_ids?: string; + start_date?: string; + end_date?: string; + filter_by?: string; +} + const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); @@ -46,31 +57,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== SUBMISSION STATE ===== const [isSubmitted, setIsSubmitted] = useState(false); - - // ===== FILTER STATE ===== - const [appliedFilterCustomer, setAppliedFilterCustomer] = useState< - typeof customerOptions - >([]); - // TODO: Uncomment when BE is ready - // const [appliedFilterSales, setAppliedFilterSales] = useState< - // typeof salesOptions - // >([]); - const [appliedFilterByType, setAppliedFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); - const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); + const [filterParams, setFilterParams] = useState({}); const [dateErrorShown, setDateErrorShown] = useState(false); const [hasDateError, setHasDateError] = useState(false); - const [filterCustomer, setFilterCustomer] = useState( - [] - ); - // TODO: Uncomment when BE is ready - // const [filterSales, setFilterSales] = useState([]); - const [filterStartDate, setFilterStartDate] = useState(''); - const [filterEndDate, setFilterEndDate] = useState(''); - const filterModal = useModal(); const dataTypeOptions = useMemo( @@ -81,10 +71,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { [] ); - const [filterByType, setFilterByType] = useState< - (typeof dataTypeOptions)[0] | null - >(null); - const { options: customerOptions, setInputValue: setCustomerInputValue, @@ -92,14 +78,43 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { loadMore: loadMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); - // TODO: Uncomment when BE is ready - // const { - // options: salesOptions, - // setInputValue: setSalesInputValue, - // isLoadingOptions: isLoadingSales, - // loadMore: loadMoreSales, - // hasMore: hasMoreSales, - // } = useSelect(UserApi.basePath, 'id', 'name', 'search'); + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + start_date: null, + end_date: null, + customer_ids: null, + filter_by: null, + }, + validationSchema: CustomerPaymentFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + customer_ids: values.customer_ids || undefined, + filter_by: values.filter_by || undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setCurrentPage(1); + setSubmitting(false); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setCurrentPage(1); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + }); const getPaymentStatusColor = (notes: string) => { const normalizedValue = notes.toLowerCase(); @@ -137,63 +152,15 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { .join(' '); }; - // ===== FILTER HANDLERS ===== - const handleFilterModalOpen = useCallback(() => { - setFilterCustomer(appliedFilterCustomer); - // setFilterSales(appliedFilterSales); - setFilterByType(appliedFilterByType); - setFilterStartDate(appliedFilterStartDate); - setFilterEndDate(appliedFilterEndDate); - filterModal.openModal(); - }, [ - filterModal, - appliedFilterCustomer, - appliedFilterByType, - appliedFilterStartDate, - appliedFilterEndDate, - ]); - - const handleResetFilters = useCallback(() => { - setIsSubmitted(false); - setFilterCustomer([]); - setFilterByType(null); - setFilterStartDate(''); - setFilterEndDate(''); - setAppliedFilterCustomer([]); - setAppliedFilterByType(null); - setAppliedFilterStartDate(''); - setAppliedFilterEndDate(''); - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }, [dateErrorShown]); - - const handleApplyFilters = useCallback(() => { - setAppliedFilterCustomer(filterCustomer); - setAppliedFilterByType(filterByType); - setAppliedFilterStartDate(filterStartDate); - setAppliedFilterEndDate(filterEndDate); - setIsSubmitted(true); - setCurrentPage(1); - filterModal.closeModal(); - }, [ - filterModal, - filterCustomer, - filterByType, - filterStartDate, - filterEndDate, - ]); - + // ===== DATE CHANGE HANDLERS ===== const handleStartDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterStartDate(value); + formik.setFieldValue('start_date', value || null); - if (value && filterEndDate) { + if (value && formik.values.end_date) { const startDate = new Date(value); - const endDateObj = new Date(filterEndDate); + const endDateObj = new Date(formik.values.end_date); if (endDateObj < startDate) { setHasDateError(true); @@ -214,16 +181,16 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setHasDateError(false); } }, - [filterEndDate, dateErrorShown] + [formik, dateErrorShown] ); const handleEndDateChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; - setFilterEndDate(value); + formik.setFieldValue('end_date', value || null); - if (value && filterStartDate) { - const startDateObj = new Date(filterStartDate); + if (value && formik.values.start_date) { + const startDateObj = new Date(formik.values.start_date); const endDate = new Date(value); if (endDate < startDateObj) { @@ -244,41 +211,46 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setDateErrorShown(false); } }, - [filterStartDate, dateErrorShown] + [formik, dateErrorShown] ); + // ===== FILTER HELPERS ===== + const customerIdsValue = useMemo(() => { + if (!formik.values.customer_ids) return []; + return customerOptions.filter((opt) => + formik.values.customer_ids?.split(',').includes(String(opt.value)) + ); + }, [formik.values.customer_ids, customerOptions]); + + const filterByValue = useMemo(() => { + if (!formik.values.filter_by) return null; + return ( + dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || + null + ); + }, [formik.values.filter_by]); + // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; // Date filter (start_date + end_date = 1 filter) - if (appliedFilterStartDate || appliedFilterEndDate) { + if (filterParams.start_date || filterParams.end_date) { count += 1; } // Customer filter - if (appliedFilterCustomer.length > 0) { + if (filterParams.customer_ids) { count += 1; } // Filter by type filter (hanya dihitung jika ada nilai yang dipilih) - if (appliedFilterByType) { + if (filterParams.filter_by) { count += 1; } - // TODO: Uncomment when BE is ready - // // Sales filter - // if (appliedFilterSales.length > 0) { - // count += 1; - // } - return count; - }, [ - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterCustomer, - appliedFilterByType, - ]); + }, [filterParams]); const hasFilters = activeFiltersCount > 0; @@ -287,21 +259,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { isSubmitted ? () => { const params = { - customer_ids: - appliedFilterCustomer.length > 0 - ? appliedFilterCustomer.map((v) => String(v.value)).join(',') - : undefined, - // TODO: Uncomment when BE is ready - // sales_id: - // appliedFilterSales.length > 0 - // ? appliedFilterSales.map((v) => String(v.value)).join(',') - // : undefined, - filter_by: appliedFilterByType?.value as + customer_ids: filterParams.customer_ids, + filter_by: filterParams.filter_by as | 'trans_date' | 'realization_date' | undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, + start_date: filterParams.start_date, + end_date: filterParams.end_date, page: currentPage, limit: pageSize, }; @@ -333,21 +297,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { CustomerPaymentReport[] | null > => { const params = { - customer_ids: - appliedFilterCustomer.length > 0 - ? appliedFilterCustomer.map((v) => String(v.value)).join(',') - : undefined, - // TODO: Uncomment when BE is ready - // sales_id: - // appliedFilterSales.length > 0 - // ? appliedFilterSales.map((v) => String(v.value)).join(',') - // : undefined, - filter_by: appliedFilterByType?.value as + customer_ids: filterParams.customer_ids, + filter_by: filterParams.filter_by as | 'trans_date' | 'realization_date' | undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, + start_date: filterParams.start_date, + end_date: filterParams.end_date, limit: 100, page: 1, }; @@ -364,13 +320,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return isResponseSuccess(response) ? (response.data as unknown as CustomerPaymentReport[]) : null; - }, [ - appliedFilterCustomer, - // appliedFilterSales, - appliedFilterStartDate, - appliedFilterEndDate, - appliedFilterByType, - ]); + }, [filterParams]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -410,21 +360,22 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return; } + const customerName = filterParams.customer_ids + ? customerOptions + .filter((opt) => + filterParams.customer_ids?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Customer' + : 'Semua Customer'; + await generateCustomerPaymentPDF({ data: allDataForExport, params: { - customer_name: - appliedFilterCustomer.length > 0 - ? appliedFilterCustomer.map((c) => c.label).join(', ') - : undefined, - // TODO: Uncomment when BE is ready - // sales: - // appliedFilterSales.length > 0 - // ? appliedFilterSales.map((s) => s.label).join(', ') - // : undefined, - start_date: appliedFilterStartDate || undefined, - end_date: appliedFilterEndDate || undefined, - filter_by: appliedFilterByType?.value as + customer_name: customerName, + start_date: filterParams.start_date, + end_date: filterParams.end_date, + filter_by: filterParams.filter_by as | 'trans_date' | 'realization_date' | undefined, @@ -436,7 +387,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } finally { setIsPdfExportLoading(false); } - }, [customerPaymentExport]); + }, [customerPaymentExport, filterParams, customerOptions]); // ===== REGISTER TAB ACTIONS TO STORE ===== const setTabActions = useFinanceTabStore((state) => state.setTabActions); @@ -517,7 +468,6 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setTabActions, ]); - // Cleanup on unmount useEffect(() => { return () => { clearTabActions(tabId); @@ -931,95 +881,86 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => {
-
-
- -
- -
+
+
+
+ +
+ +
- + +
+ + { + formik.setFieldValue( + 'customer_ids', + Array.isArray(val) && val.length > 0 + ? val.map((v: OptionType) => String(v.value)).join(',') + : null + ); + }} + onInputChange={setCustomerInputValue} + isLoading={isLoadingCustomers} + isClearable + onMenuScrollToBottom={loadMoreCustomers} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('filter_by', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + />
- { - setFilterCustomer(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setCustomerInputValue} - isLoading={isLoadingCustomers} - isClearable - onMenuScrollToBottom={loadMoreCustomers} - className={{ wrapper: 'w-full' }} - /> - - {/* TODO: Uncomment when BE is ready */} - {/*
- { - setFilterSales(Array.isArray(val) ? val : val ? [val] : []); - }} - onInputChange={setSalesInputValue} - isLoading={isLoadingSales} - isClearable - onMenuScrollToBottom={loadMoreSales} - className={{ wrapper: 'w-full' }} - /> -
*/} - - { - if (val && !Array.isArray(val)) { - setFilterByType(val); - } - }} - className={{ wrapper: 'w-full' }} - /> - - {/* Action Buttons */} -
-
- - -
+ {/* Modal Footer */} +
+ + +
+ ); From e23b53d7979d6eb64bab5afd6c98c113d041b548 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 10:46:06 +0700 Subject: [PATCH 068/149] refactor(FE): Refactor CustomerPaymentTab to use StatusBadge component --- .../report/finance/tab/CustomerPaymentTab.tsx | 60 ++++++------------- 1 file changed, 19 insertions(+), 41 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 723a1ebf..a0fb63ae 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -2,7 +2,7 @@ import { useState, useMemo, useCallback, useEffect } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; -import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import { useSelect } from '@/components/input/SelectInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; @@ -11,7 +11,13 @@ import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; -import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; +import { + formatCurrency, + formatDate, + formatNumber, + formatTitleCase, + cn, +} from '@/lib/helper'; import { CustomerPaymentReport, CustomerPaymentSummary, @@ -33,6 +39,7 @@ import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/ex import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { OptionType } from '@/components/table/TableRowSizeSelector'; +import { Color } from '@/types/theme'; interface CustomerPaymentTabProps { tabId: string; @@ -116,40 +123,18 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { }, }); - const getPaymentStatusColor = (notes: string) => { + const getPaymentStatusBadgeColor = (notes: string): Color => { const normalizedValue = notes.toLowerCase(); if (normalizedValue === 'lunas') { - return 'bg-info/10 text-black border-info'; + return 'primary'; } if (normalizedValue.includes('belum')) { - return 'bg-warning/10 text-black border-warning'; + return 'warning'; } - return 'bg-gray-100 text-black border-gray-300'; - }; - - const getPaymentStatusIndicatorColor = (notes: string) => { - const normalizedValue = notes.toLowerCase(); - - if (normalizedValue === 'lunas') { - return 'bg-info'; - } - - if (normalizedValue.includes('belum')) { - return 'bg-warning'; - } - - return 'bg-gray-400'; - }; - - const getPaymentStatusText = (notes: string) => { - return notes - .toLowerCase() - .split(' ') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); + return 'neutral'; }; // ===== DATE CHANGE HANDLERS ===== @@ -181,7 +166,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setHasDateError(false); } }, - [formik, dateErrorShown] + [dateErrorShown] ); const handleEndDateChange = useCallback( @@ -211,7 +196,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setDateErrorShown(false); } }, - [formik, dateErrorShown] + [dateErrorShown] ); // ===== FILTER HELPERS ===== @@ -685,17 +670,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { } return ( - - {getPaymentStatusText(value)} - + ); }, }, From 322b519def4db1b92a3aee81863db64092f24ba8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 10:55:40 +0700 Subject: [PATCH 069/149] refactor(FE): Refactor filter schema and form handling for PurchasesPerSupplier --- .../filter/PurchasesPerSupplierFilter.ts | 110 ++++---------- .../tab/PurchasesPerSupplierTab.tsx | 137 ++++++++++-------- 2 files changed, 106 insertions(+), 141 deletions(-) diff --git a/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts index 70a01615..b3d9943b 100644 --- a/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts +++ b/src/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter.ts @@ -1,88 +1,38 @@ -import { OptionType } from '@/components/input/SelectInput'; import * as yup from 'yup'; export type PurchasesPerSupplierFilterType = { - start_date: string | null | undefined; - end_date: string | null | undefined; - area_ids: OptionType[] | null | undefined; - supplier_ids: OptionType[] | null | undefined; - product_ids: OptionType[] | null | undefined; - product_category_ids: OptionType[] | null | undefined; - filter_by: OptionType | null | undefined; - sort_by: OptionType | null | undefined; + start_date: string | null; + end_date: string | null; + area_ids: string | null; + supplier_ids: string | null; + product_ids: string | null; + product_category_ids: string | null; + filter_by: string | null; + sort_by: string | null; }; -export const PurchasesPerSupplierFilterSchema: yup.ObjectSchema = - yup.object({ - start_date: yup.string().optional().nullable(), - end_date: yup - .string() - .optional() - .nullable() - .test( - 'is-greater-than-start', - 'Tanggal akhir tidak boleh masa lampau', - function (value) { - const { start_date } = this.parent; - if (!start_date || !value) return true; - return new Date(value) >= new Date(start_date); - } - ), - area_ids: yup - .array() - .of( - yup.object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - ) - .optional() - .nullable(), - supplier_ids: yup - .array() - .of( - yup.object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - ) - .optional() - .nullable(), - product_ids: yup - .array() - .of( - yup.object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - ) - .optional() - .nullable(), - product_category_ids: yup - .array() - .of( - yup.object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - ) - .optional() - .nullable(), - filter_by: yup - .object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - .optional() - .nullable(), - sort_by: yup - .object({ - value: yup.mixed().required(), - label: yup.string().required(), - }) - .optional() - .nullable(), - }); +export const PurchasesPerSupplierFilterSchema = yup.object({ + start_date: yup.string().optional().nullable(), + end_date: yup + .string() + .optional() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + area_ids: yup.string().nullable(), + supplier_ids: yup.string().nullable(), + product_ids: yup.string().nullable(), + product_category_ids: yup.string().nullable(), + filter_by: yup.string().nullable(), + sort_by: yup.string().nullable(), +}); export type PurchasesPerSupplierFilterValues = yup.InferType< typeof PurchasesPerSupplierFilterSchema diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 794c45d6..23fb067e 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -125,21 +125,14 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { validationSchema: PurchasesPerSupplierFilterSchema, onSubmit: (values, { setSubmitting }) => { setFilterParams({ - start_date: values.start_date?.toString() || undefined, - end_date: values.end_date?.toString() || undefined, - area_id: - values.area_ids?.map((v) => String(v.value)).join(',') || undefined, - supplier_id: - values.supplier_ids?.map((v) => String(v.value)).join(',') || - undefined, - product_id: - values.product_ids?.map((v) => String(v.value)).join(',') || - undefined, - product_category_id: - values.product_category_ids?.map((v) => String(v.value)).join(',') || - undefined, - filter_by: values.filter_by?.value?.toString() || undefined, - sort_by: values.sort_by?.value?.toString() || undefined, + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + area_id: values.area_ids || undefined, + supplier_id: values.supplier_ids || undefined, + product_id: values.product_ids || undefined, + product_category_id: values.product_category_ids || undefined, + filter_by: values.filter_by || undefined, + sort_by: values.sort_by || undefined, }); filterModal.closeModal(); setIsSubmitted(true); @@ -220,6 +213,48 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { [formik, dateErrorShown] ); + // ===== DERIVED VALUES ===== + const areaIdsValue = useMemo(() => { + if (!formik.values.area_ids) return []; + const ids = formik.values.area_ids.split(','); + return areaOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.area_ids, areaOptions]); + + const supplierIdsValue = useMemo(() => { + if (!formik.values.supplier_ids) return []; + const ids = formik.values.supplier_ids.split(','); + return supplierOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.supplier_ids, supplierOptions]); + + const productIdsValue = useMemo(() => { + if (!formik.values.product_ids) return []; + const ids = formik.values.product_ids.split(','); + return productOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.product_ids, productOptions]); + + const productCategoryIdsValue = useMemo(() => { + if (!formik.values.product_category_ids) return []; + const ids = formik.values.product_category_ids.split(','); + return productCategoryOptions.filter((opt) => + ids.includes(String(opt.value)) + ); + }, [formik.values.product_category_ids, productCategoryOptions]); + + const filterByValue = useMemo(() => { + if (!formik.values.filter_by) return null; + return ( + dataTypeOptions.find((opt) => opt.value === formik.values.filter_by) || + null + ); + }, [formik.values.filter_by, dataTypeOptions]); + + const sortByValue = useMemo(() => { + if (!formik.values.sort_by) return null; + return ( + sortByOptions.find((opt) => opt.value === formik.values.sort_by) || null + ); + }, [formik.values.sort_by, sortByOptions]); + // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; @@ -875,17 +910,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Area' placeholder='Pilih Area' options={areaOptions} - value={ - (formik.values.area_ids as - | { value: number; label: string } - | { value: number; label: string }[] - | null - | undefined) || [] - } + value={areaIdsValue} onChange={(val) => { formik.setFieldValue( 'area_ids', - Array.isArray(val) ? val : val ? [val] : null + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null ); }} isLoading={isLoadingAreas} @@ -898,17 +929,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Supplier' placeholder='Pilih Supplier' options={supplierOptions} - value={ - (formik.values.supplier_ids as - | { value: number; label: string } - | { value: number; label: string }[] - | null - | undefined) || [] - } + value={supplierIdsValue} onChange={(val) => { formik.setFieldValue( 'supplier_ids', - Array.isArray(val) ? val : val ? [val] : null + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null ); }} isLoading={isLoadingSuppliers} @@ -921,17 +948,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Produk' placeholder='Pilih Produk' options={productOptions} - value={ - (formik.values.product_ids as - | { value: number; label: string } - | { value: number; label: string }[] - | null - | undefined) || [] - } + value={productIdsValue} onChange={(val) => { formik.setFieldValue( 'product_ids', - Array.isArray(val) ? val : val ? [val] : null + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null ); }} isLoading={isLoadingProducts} @@ -944,17 +967,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Kategori Produk' placeholder='Pilih Kategori Produk' options={productCategoryOptions} - value={ - (formik.values.product_category_ids as - | { value: number; label: string } - | { value: number; label: string }[] - | null - | undefined) || [] - } + value={productCategoryIdsValue} onChange={(val) => { formik.setFieldValue( 'product_category_ids', - Array.isArray(val) ? val : val ? [val] : null + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null ); }} isLoading={isLoadingProductCategories} @@ -967,15 +986,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Filter Berdasarkan' placeholder='Pilih Filter Berdasarkan' options={dataTypeOptions} - value={ - (formik.values.filter_by as - | { value: string; label: string } - | null - | undefined) || null - } + value={filterByValue} onChange={(val) => { if (!Array.isArray(val)) { - formik.setFieldValue('filter_by', val); + formik.setFieldValue( + 'filter_by', + val?.value?.toString() || null + ); } }} className={{ wrapper: 'w-full' }} @@ -987,15 +1004,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { label='Urutkan Berdasarkan' placeholder='Pilih Urutkan Berdasarkan' options={sortByOptions} - value={ - (formik.values.sort_by as - | { value: string; label: string } - | null - | undefined) || null - } + value={sortByValue} onChange={(val) => { if (!Array.isArray(val)) { - formik.setFieldValue('sort_by', val); + formik.setFieldValue( + 'sort_by', + val?.value?.toString() || null + ); } }} className={{ wrapper: 'w-full' }} From ee53ea61ccbee946516d9942b458c1cfcf71bee4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 10:57:22 +0700 Subject: [PATCH 070/149] refactor(FE): Fix missing dependency in useCallback hooks --- .../pages/report/finance/tab/CustomerPaymentTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index a0fb63ae..3680a41c 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -166,7 +166,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setHasDateError(false); } }, - [dateErrorShown] + [formik, dateErrorShown] ); const handleEndDateChange = useCallback( @@ -196,7 +196,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setDateErrorShown(false); } }, - [dateErrorShown] + [formik, dateErrorShown] ); // ===== FILTER HELPERS ===== From 6d2855d1173c373fdfe3411ff4b3d2d150580fd8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 11:15:42 +0700 Subject: [PATCH 071/149] refactor(FE): Add zustand store for marketing tab actions --- .../marketing-tab/marketing-tab.store.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/stores/marketing-tab/marketing-tab.store.ts diff --git a/src/stores/marketing-tab/marketing-tab.store.ts b/src/stores/marketing-tab/marketing-tab.store.ts new file mode 100644 index 00000000..153bbb8d --- /dev/null +++ b/src/stores/marketing-tab/marketing-tab.store.ts @@ -0,0 +1,51 @@ +'use client'; + +import { ReactNode } from 'react'; +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +export type MarketingTabActionsSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const useMarketingTabStore = create()( + devtools( + (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set( + (state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + }), + false, + 'setTabActions' + ), + + clearTabActions: (tabId) => + set( + (state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }, + false, + 'clearTabActions' + ), + + clearAllTabActions: () => + set({ tabActions: {} }, false, 'clearAllTabActions'), + }), + { + name: 'MarketingTabStore', + } + ) +); From 43d26b4833d16d99c857c54e33735e4410dde72c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 11:16:26 +0700 Subject: [PATCH 072/149] refactor(FE): Refactor marketing report components and add HPP filter --- src/app/report/marketing/page.tsx | 6 +- .../marketing/MarketingReportContent.tsx | 59 +- .../marketing/filter/HppPerKandangFilter.ts | 40 + .../skeleton/HppPerKandangSkeleton.tsx | 37 + .../report/marketing/tab/HppPerKandangTab.tsx | 919 +++++++++++------- 5 files changed, 658 insertions(+), 403 deletions(-) create mode 100644 src/components/pages/report/marketing/filter/HppPerKandangFilter.ts create mode 100644 src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx index 87ed7a1a..57035844 100644 --- a/src/app/report/marketing/page.tsx +++ b/src/app/report/marketing/page.tsx @@ -1,11 +1,7 @@ import MarketingReportContent from '@/components/pages/report/marketing/MarketingReportContent'; const MarketingReportPage = () => { - return ( -
- -
- ); + return ; }; export default MarketingReportPage; diff --git a/src/components/pages/report/marketing/MarketingReportContent.tsx b/src/components/pages/report/marketing/MarketingReportContent.tsx index e38a39d4..9277311f 100644 --- a/src/components/pages/report/marketing/MarketingReportContent.tsx +++ b/src/components/pages/report/marketing/MarketingReportContent.tsx @@ -1,47 +1,42 @@ 'use client'; -import { JSX, useState } from 'react'; - +import { useState } from 'react'; import Tabs from '@/components/Tabs'; import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingReportContent'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; - -type MarketingReportTabType = - | 'daily' - | 'transaction' - | 'hpp-comparison' - | 'daily-hpp'; - -const marketingReportTabs: { - id: MarketingReportTabType; - label: string; - content: JSX.Element; -}[] = [ - { - id: 'daily', - label: 'Penjualan Harian', - content: , - }, - { - id: 'daily-hpp', - label: 'HPP Harian Kandang', - content: , - }, -]; +import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; const MarketingReportContent = () => { - const [activeTab, setActiveTab] = useState('daily'); + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useMarketingTabStore((state) => state.tabActions); + + const tabs = [ + { + id: '1', + label: 'Penjualan Harian', + content: , + }, + { + id: '2', + label: 'HPP Harian Kandang', + content: , + }, + ]; return ( -
+
); diff --git a/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts b/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts new file mode 100644 index 00000000..57d2dcd2 --- /dev/null +++ b/src/components/pages/report/marketing/filter/HppPerKandangFilter.ts @@ -0,0 +1,40 @@ +import * as yup from 'yup'; + +export type HppPerKandangFilterType = { + area_id: string | null; + location_id: string | null; + kandang_id: string | null; + weight_min: string | null; + weight_max: string | null; + period: string | null; + sort_by: string | null; + show_unrecorded: boolean | null; +}; + +export const HppPerKandangFilterSchema = yup.object({ + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + kandang_id: yup.string().nullable(), + weight_min: yup.string().nullable(), + weight_max: yup + .string() + .nullable() + .test( + 'is-greater-than-min', + 'Rentang bobot max tidak boleh lebih kecil dari min', + function (value) { + const { weight_min } = this.parent; + if (!weight_min || !value) return true; + const weightMinNum = parseFloat(weight_min) || 0; + const weightMaxNum = parseFloat(value) || 0; + return weightMaxNum >= weightMinNum; + } + ), + period: yup.string().required('Periode wajib diisi'), + sort_by: yup.string().nullable(), + show_unrecorded: yup.boolean().nullable(), +}); + +export type HppPerKandangFilterValues = yup.InferType< + typeof HppPerKandangFilterSchema +>; diff --git a/src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx b/src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx new file mode 100644 index 00000000..42a6cf56 --- /dev/null +++ b/src/components/pages/report/marketing/skeleton/HppPerKandangSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang'; +import { ColumnDef } from '@tanstack/react-table'; + +const HppPerKandangSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default HppPerKandangSkeleton; diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index c0371abf..514edcb9 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -1,11 +1,6 @@ import { useState, useMemo, useCallback } from 'react'; -import { ChangeEventHandler } from 'react'; import useSWR from 'swr'; -import Card from '@/components/Card'; -import SelectInput, { - useSelect, - OptionType, -} from '@/components/input/SelectInput'; +import { useSelect } from '@/components/input/SelectInput'; import DateInput from '@/components/input/DateInput'; import NumberInput from '@/components/input/NumberInput'; import { AreaApi } from '@/services/api/master-data'; @@ -21,7 +16,6 @@ import { HppPerKandangPerWeightRange, } from '@/types/api/report/hpp-per-kandang'; import { isResponseSuccess } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; @@ -30,8 +24,35 @@ import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/ex import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; +import { useFormik } from 'formik'; +import { + HppPerKandangFilterSchema, + HppPerKandangFilterType, +} from '@/components/pages/report/marketing/filter/HppPerKandangFilter'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; +import Modal, { useModal } from '@/components/Modal'; +import { cn } from '@/lib/helper'; +import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; +import { useEffect as useEffectHook } from 'react'; -const HppPerKandangTab = () => { +interface HppPerKandangTabProps { + tabId: string; +} + +interface FilterParams { + area_id?: string; + location_id?: string; + kandang_id?: string; + weight_min?: string; + weight_max?: string; + period?: string; + sort_by?: string; + show_unrecorded?: boolean; +} + +const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); @@ -42,190 +63,229 @@ const HppPerKandangTab = () => { // ===== VALIDATION STATE ===== const [weightMaxError, setWeightMaxError] = useState(''); + const [dateErrorShown, setDateErrorShown] = useState(false); - // ===== TABLE FILTER STATE ===== - const { state: tableFilterState, updateFilter } = useTableFilter({ - initial: { - area_id: [] as string[], - location_id: [] as string[], - kandang_id: [] as string[], - weight_min: '', - weight_max: '', - period: '', - sort_by: '', - show_unrecorded: false, - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); + // ===== FILTER STATE ===== + const [filterParams, setFilterParams] = useState({}); - const { - setInputValue: setAreaInputValue, - options: areaOptions, - isLoadingOptions: isLoadingAreas, - loadMore: loadMoreAreas, - } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); + const filterModal = useModal(); - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocations, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); - - const { - setInputValue: setKandangInputValue, - options: kandangOptions, - isLoadingOptions: isLoadingKandangs, - loadMore: loadMoreKandangs, - } = useSelect( - ProjectFlockKandangApi.basePath, + // ===== OPTIONS ===== + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, 'id', - 'name_with_period', + 'name', 'search' ); - const showUnrecordedOptions: OptionType[] = [ - { value: 'false', label: 'Sembunyikan' }, - { value: 'true', label: 'Tampilkan' }, - ]; + const { options: locationOptions, isLoadingOptions: isLoadingLocations } = + useSelect(LocationApi.basePath, 'id', 'name', 'search'); - const areaChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'area_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); - }, - [updateFilter] + const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = + useSelect( + ProjectFlockKandangApi.basePath, + 'id', + 'name_with_period', + 'search' + ); + + const showUnrecordedOptions = useMemo( + () => [ + { value: 'false', label: 'Sembunyikan' }, + { value: 'true', label: 'Tampilkan' }, + ], + [] ); - const locationChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'location_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + area_id: null, + location_id: null, + kandang_id: null, + weight_min: null, + weight_max: null, + period: null, + sort_by: null, + show_unrecorded: null, }, - [updateFilter] - ); - - const kandangChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - updateFilter( - 'kandang_id', - arr.map((v) => String((v as OptionType).value)) - ); - setIsSubmitted(false); + validationSchema: HppPerKandangFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + area_id: values.area_id || undefined, + location_id: values.location_id || undefined, + kandang_id: values.kandang_id || undefined, + weight_min: values.weight_min || undefined, + weight_max: values.weight_max || undefined, + period: values.period || undefined, + sort_by: values.sort_by || undefined, + show_unrecorded: + values.show_unrecorded !== null ? values.show_unrecorded : undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setSubmitting(false); }, - [updateFilter] - ); - - const weightMinChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - updateFilter('weight_min', val ? String(parseFloat(val) || 0) : ''); + onReset: () => { + setFilterParams({}); setIsSubmitted(false); + setWeightMaxError(''); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + }); - if (weightMaxError) { + // ===== WEIGHT CHANGE HANDLERS ===== + const handleWeightMinChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('weight_min', value || null); + + if (value && formik.values.weight_max) { + const weightMin = parseFloat(value) || 0; + const weightMax = parseFloat(formik.values.weight_max) || 0; + + if (weightMax < weightMin) { + setWeightMaxError( + 'Rentang bobot max tidak boleh lebih kecil dari min' + ); + if (!dateErrorShown) { + toast.error('Rentang bobot max tidak boleh lebih kecil dari min', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setWeightMaxError(''); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { setWeightMaxError(''); } }, - [updateFilter, weightMaxError] + [formik, dateErrorShown] ); - const weightMaxChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const val = e.target.value; - const weightMax = val ? parseFloat(val) || 0 : 0; - const weightMin = tableFilterState.weight_min - ? parseFloat(tableFilterState.weight_min) - : 0; + const handleWeightMaxChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + formik.setFieldValue('weight_max', value || null); - if (weightMax < weightMin) { - setWeightMaxError('Rentang bobot max tidak boleh lebih kecil dari min'); - toast.error('Rentang bobot max tidak boleh lebih kecil dari min'); - return; + if (value && formik.values.weight_min) { + const weightMin = parseFloat(formik.values.weight_min) || 0; + const weightMax = parseFloat(value) || 0; + + if (weightMax < weightMin) { + setWeightMaxError( + 'Rentang bobot max tidak boleh lebih kecil dari min' + ); + if (!dateErrorShown) { + toast.error('Rentang bobot max tidak boleh lebih kecil dari min', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } } setWeightMaxError(''); - updateFilter('weight_max', val ? String(weightMax) : ''); - setIsSubmitted(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }, - [updateFilter, tableFilterState.weight_min] + [formik, dateErrorShown] ); - const periodChangeHandler = useCallback>( - (e) => { - const val = e.target.value; - updateFilter('period', val || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); + // ===== DERIVED VALUES ===== + const areaIdsValue = useMemo(() => { + if (!formik.values.area_id) return []; + const ids = formik.values.area_id.split(','); + return areaOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.area_id, areaOptions]); - const showUnrecordedChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - updateFilter('show_unrecorded', newVal?.value === 'true'); - setIsSubmitted(false); - }, - [updateFilter] - ); + const locationIdsValue = useMemo(() => { + if (!formik.values.location_id) return []; + const ids = formik.values.location_id.split(','); + return locationOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.location_id, locationOptions]); - const resetFilters = useCallback(() => { - updateFilter('area_id', []); - updateFilter('location_id', []); - updateFilter('kandang_id', []); - updateFilter('weight_min', ''); - updateFilter('weight_max', ''); - updateFilter('period', ''); - updateFilter('sort_by', ''); - updateFilter('show_unrecorded', false); - setIsSubmitted(false); - }, [updateFilter]); + const kandangIdsValue = useMemo(() => { + if (!formik.values.kandang_id) return []; + const ids = formik.values.kandang_id.split(','); + return kandangOptions.filter((opt) => ids.includes(String(opt.value))); + }, [formik.values.kandang_id, kandangOptions]); - const handleSubmit = useCallback(() => { - if (!tableFilterState.period) { - toast.error('Periode wajib diisi'); - return; + const showUnrecordedValue = useMemo(() => { + if (formik.values.show_unrecorded === null) return null; + return ( + showUnrecordedOptions.find( + (opt) => opt.value === String(formik.values.show_unrecorded) + ) || null + ); + }, [formik.values.show_unrecorded, showUnrecordedOptions]); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (filterParams.period) { + count += 1; } - setIsSubmitted(true); - }, [tableFilterState.period]); + + if (filterParams.area_id) { + count += 1; + } + + if (filterParams.location_id) { + count += 1; + } + + if (filterParams.kandang_id) { + count += 1; + } + + if (filterParams.weight_min || filterParams.weight_max) { + count += 1; + } + + if (filterParams.show_unrecorded !== undefined) { + count += 1; + } + + if (filterParams.sort_by) { + count += 1; + } + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; // ===== DATA FETCHING ===== const { data: hppPerKandang, isLoading } = useSWR( isSubmitted ? () => { const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - location_id: - tableFilterState.location_id.length > 0 - ? tableFilterState.location_id.join(',') - : undefined, - kandang_id: - tableFilterState.kandang_id.length > 0 - ? tableFilterState.kandang_id.join(',') - : undefined, - weight_min: tableFilterState.weight_min || undefined, - weight_max: tableFilterState.weight_max || undefined, - period: tableFilterState.period || undefined, - sort_by: tableFilterState.sort_by || undefined, - show_unrecorded: tableFilterState.show_unrecorded, + area_id: filterParams.area_id, + location_id: filterParams.location_id, + kandang_id: filterParams.kandang_id, + weight_min: filterParams.weight_min, + weight_max: filterParams.weight_max, + period: filterParams.period, + sort_by: filterParams.sort_by, + show_unrecorded: filterParams.show_unrecorded, }; return ['hpp-per-kandang-report', params]; @@ -275,23 +335,14 @@ const HppPerKandangTab = () => { const hppPerKandangExport = useCallback(async (): Promise => { const params = { - area_id: - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id.join(',') - : undefined, - location_id: - tableFilterState.location_id.length > 0 - ? tableFilterState.location_id.join(',') - : undefined, - kandang_id: - tableFilterState.kandang_id.length > 0 - ? tableFilterState.kandang_id.join(',') - : undefined, - weight_min: tableFilterState.weight_min || undefined, - weight_max: tableFilterState.weight_max || undefined, - period: tableFilterState.period || undefined, - sort_by: tableFilterState.sort_by || undefined, - show_unrecorded: tableFilterState.show_unrecorded, + area_id: filterParams.area_id, + location_id: filterParams.location_id, + kandang_id: filterParams.kandang_id, + weight_min: filterParams.weight_min, + weight_max: filterParams.weight_max, + period: filterParams.period, + sort_by: filterParams.sort_by, + show_unrecorded: filterParams.show_unrecorded, limit: 10000, page: 1, }; @@ -308,7 +359,7 @@ const HppPerKandangTab = () => { ); return isResponseSuccess(response) ? response.data : null; - }, [tableFilterState]); + }, [filterParams]); // ===== TABLE COLUMNS DEFINITION ===== const allFeedSuppliers = useMemo(() => { @@ -373,38 +424,32 @@ const HppPerKandangTab = () => { return; } - const areaName = - tableFilterState.area_id.length > 0 - ? tableFilterState.area_id - .map( - (id) => - areaOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Area' - : 'Semua Area'; + const areaName = filterParams.area_id + ? areaOptions + .filter((opt) => + filterParams.area_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Area' + : 'Semua Area'; - const locationName = - tableFilterState.location_id.length > 0 - ? tableFilterState.location_id - .map( - (id) => - locationOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Lokasi' - : 'Semua Lokasi'; + const locationName = filterParams.location_id + ? locationOptions + .filter((opt) => + filterParams.location_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Lokasi' + : 'Semua Lokasi'; - const kandangName = - tableFilterState.kandang_id.length > 0 - ? tableFilterState.kandang_id - .map( - (id) => - kandangOptions.find((opt) => opt.value === Number(id))?.label - ) - .filter(Boolean) - .join(', ') || 'Semua Kandang' - : 'Semua Kandang'; + const kandangName = filterParams.kandang_id + ? kandangOptions + .filter((opt) => + filterParams.kandang_id?.split(',').includes(String(opt.value)) + ) + .map((opt) => opt.label) + .join(', ') || 'Semua Kandang' + : 'Semua Kandang'; await generateHppPerKandangPDF( { @@ -413,11 +458,12 @@ const HppPerKandangTab = () => { area_name: areaName, location_name: locationName, kandang_name: kandangName, - period: tableFilterState.period, - weight_min: tableFilterState.weight_min, - weight_max: tableFilterState.weight_max, - show_unrecorded: tableFilterState.show_unrecorded.toString(), - sort_by: tableFilterState.sort_by, + period: filterParams.period, + weight_min: filterParams.weight_min, + weight_max: filterParams.weight_max, + show_unrecorded: + filterParams.show_unrecorded?.toString() || 'false', + sort_by: filterParams.sort_by, }, }, allFeedSuppliers, @@ -432,7 +478,7 @@ const HppPerKandangTab = () => { } }, [ hppPerKandangExport, - tableFilterState, + filterParams, areaOptions, locationOptions, kandangOptions, @@ -440,6 +486,91 @@ const HppPerKandangTab = () => { allDocSuppliers, ]); + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useMarketingTabStore((state) => state.setTabActions); + const clearTabActions = useMarketingTabStore( + (state) => state.clearTabActions + ); + + useEffectHook(() => { + setTabActions( + tabId, +
+ + + + + Export +
+ +
+ + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
+
+ ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + filterModal.open, + setTabActions, + ]); + + useEffectHook(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + const getTableColumns = (): ColumnDef[] => { const tableColumns: ColumnDef[] = [ { @@ -682,161 +813,50 @@ const HppPerKandangTab = () => { ); return ( -
- HPP Harian Kandang (${period})` - : 'Laporan > HPP Harian Kandang' - } - className={{ wrapper: 'w-full', body: 'p-1!' }} - > -
- - (tableFilterState.area_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={areaChangeHandler} - onInputChange={setAreaInputValue} - onMenuScrollToBottom={loadMoreAreas} - isLoading={isLoadingAreas} - closeMenuOnSelect={false} - hideSelectedOptions={false} - isClearable - /> - - (tableFilterState.location_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={locationChangeHandler} - onInputChange={setLocationInputValue} - onMenuScrollToBottom={loadMoreLocations} - isLoading={isLoadingLocations} - closeMenuOnSelect={false} - hideSelectedOptions={false} - isClearable - /> - - (tableFilterState.kandang_id || []) - .map(String) - .includes(String(opt.value)) - )} - onChange={kandangChangeHandler} - onInputChange={setKandangInputValue} - onMenuScrollToBottom={loadMoreKandangs} - isLoading={isLoadingKandangs} - closeMenuOnSelect={false} - hideSelectedOptions={false} - isClearable - /> -
- -
-
- - -
- - opt.value === 'true') || - null - : showUnrecordedOptions.find((opt) => opt.value === 'false') || - null - } - onChange={showUnrecordedChangeHandler} - /> -
- -
- - - - Export - - - } - align='end' - > - - - - - -
- -
- + <> +
{!isSubmitted ? ( -
- Silakan pilih filter dan klik tombol Cari untuk menampilkan data. -
+ + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> ) : isLoading ? ( -
- -
+ + } + title='Memuat Data HPP Per Kandang' + subtitle='Silakan tunggu sebentar...' + /> ) : data.length === 0 ? ( -
- Tidak ada data yang dapat ditampilkan... -
+ + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> ) : (
{ renderFooter={data.length > 0} renderCustomRow={renderCustomRow} className={{ - containerClassName: 'w-full mt-6', - tableWrapperClassName: 'overflow-x-auto mt-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: 'overflow-x-auto', tableClassName: 'w-full table-auto text-sm', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: @@ -862,8 +882,175 @@ const HppPerKandangTab = () => { }} /> )} - - + + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ {/* Period Filter */} +
+ { + formik.setFieldValue('period', e.target.value || null); + }} + className={{ wrapper: 'w-full' }} + isNestedModal + required={true} + isError={!!formik.errors.period && formik.touched.period} + /> + {formik.errors.period && formik.touched.period && ( +
+ {formik.errors.period} +
+ )} +
+ + {/* Area Filter */} + { + formik.setFieldValue( + 'area_id', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Location Filter */} + { + formik.setFieldValue( + 'location_id', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingLocations} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Kandang Filter */} + { + formik.setFieldValue( + 'kandang_id', + Array.isArray(val) && val.length > 0 + ? val.map((v) => String(v.value)).join(',') + : null + ); + }} + isLoading={isLoadingKandangs} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Weight Range Filter */} +
+ +
+ +
+ +
+
+ + {/* Show Unrecorded Filter */} + { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'show_unrecorded', + val?.value === 'true' || null + ); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> +
+ + {/* Modal Footer */} +
+ + +
+ +
+ ); }; From 5e4619fac7cdee9f516509e28c23c69456d3d2a3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 11:38:46 +0700 Subject: [PATCH 073/149] feat(FE): Add DailyMarketingReportFilter and DailyMarketingReportSkeleton components --- .../filter/DailyMarketingReportFilter.ts | 42 +++++++++++++++++++ .../skeleton/DailyMarketingReportSkeleton.tsx | 37 ++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts create mode 100644 src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx diff --git a/src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts b/src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts new file mode 100644 index 00000000..85c765a9 --- /dev/null +++ b/src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts @@ -0,0 +1,42 @@ +import * as yup from 'yup'; + +export type DailyMarketingReportFilterType = { + search: string | null; + area_id: string | null; + location_id: string | null; + warehouse_id: string | null; + customer_id: string | null; + start_date: string | null; + end_date: string | null; + marketing_type: string | null; + filter_by: string | null; + sort_by: string | null; +}; + +export const DailyMarketingReportFilterSchema = yup.object({ + search: yup.string().nullable(), + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + warehouse_id: yup.string().nullable(), + customer_id: yup.string().nullable(), + start_date: yup.string().nullable(), + end_date: yup + .string() + .nullable() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), + marketing_type: yup.string().nullable(), + filter_by: yup.string().nullable(), + sort_by: yup.string().nullable(), +}); + +export type DailyMarketingReportFilterValues = yup.InferType< + typeof DailyMarketingReportFilterSchema +>; diff --git a/src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx b/src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx new file mode 100644 index 00000000..ad68b8f6 --- /dev/null +++ b/src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { DailyMarketingRow } from '@/types/api/report/marketing.d'; +import { ColumnDef } from '@tanstack/react-table'; + +const DailyMarketingReportSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default DailyMarketingReportSkeleton; From 4b6a8b27731123e4a8c1c07b51d26552b68e41fd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 11:45:50 +0700 Subject: [PATCH 074/149] refactor(FE): Refactor file names for consistency in marketing report components --- .../pages/report/marketing/MarketingReportContent.tsx | 2 +- .../pages/report/marketing/export/DailyMarketingExportXLSX.tsx | 0 .../{DailyMarketingReportPDF.tsx => DailyMarketingPDF.tsx} | 0 .../{DailyMarketingReportFilter.ts => DailyMarketingFilter.ts} | 0 ...lyMarketingReportSkeleton.tsx => DailyMarketingSkeleton.tsx} | 0 .../{DailyMarketingReportContent.tsx => DailyMarketingTab.tsx} | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx rename src/components/pages/report/marketing/export/{DailyMarketingReportPDF.tsx => DailyMarketingPDF.tsx} (100%) rename src/components/pages/report/marketing/filter/{DailyMarketingReportFilter.ts => DailyMarketingFilter.ts} (100%) rename src/components/pages/report/marketing/skeleton/{DailyMarketingReportSkeleton.tsx => DailyMarketingSkeleton.tsx} (100%) rename src/components/pages/report/marketing/tab/{DailyMarketingReportContent.tsx => DailyMarketingTab.tsx} (99%) diff --git a/src/components/pages/report/marketing/MarketingReportContent.tsx b/src/components/pages/report/marketing/MarketingReportContent.tsx index 9277311f..8a814689 100644 --- a/src/components/pages/report/marketing/MarketingReportContent.tsx +++ b/src/components/pages/report/marketing/MarketingReportContent.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; -import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingReportContent'; +import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingPDF.tsx similarity index 100% rename from src/components/pages/report/marketing/export/DailyMarketingReportPDF.tsx rename to src/components/pages/report/marketing/export/DailyMarketingPDF.tsx diff --git a/src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts b/src/components/pages/report/marketing/filter/DailyMarketingFilter.ts similarity index 100% rename from src/components/pages/report/marketing/filter/DailyMarketingReportFilter.ts rename to src/components/pages/report/marketing/filter/DailyMarketingFilter.ts diff --git a/src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx b/src/components/pages/report/marketing/skeleton/DailyMarketingSkeleton.tsx similarity index 100% rename from src/components/pages/report/marketing/skeleton/DailyMarketingReportSkeleton.tsx rename to src/components/pages/report/marketing/skeleton/DailyMarketingSkeleton.tsx diff --git a/src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx similarity index 99% rename from src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx rename to src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index ca5ec12f..9f1d6d42 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingReportContent.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -16,7 +16,7 @@ import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingReportPDF'; +import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingPDF'; import { Area } from '@/types/api/master-data/area'; import { From 325fb373a8ba6a3f7ff5720a4b1e0c1c265e528c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 13:19:16 +0700 Subject: [PATCH 075/149] refactor(FE): Rename components for clarity --- src/app/report/marketing/page.tsx | 2 +- .../marketing/{MarketingReportContent.tsx => MarketingTabs.tsx} | 0 .../{DailyMarketingPDF.tsx => DailyMarketingExportPDF.tsx} | 0 src/components/pages/report/marketing/tab/DailyMarketingTab.tsx | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename src/components/pages/report/marketing/{MarketingReportContent.tsx => MarketingTabs.tsx} (100%) rename src/components/pages/report/marketing/export/{DailyMarketingPDF.tsx => DailyMarketingExportPDF.tsx} (100%) diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx index 57035844..cb79f109 100644 --- a/src/app/report/marketing/page.tsx +++ b/src/app/report/marketing/page.tsx @@ -1,4 +1,4 @@ -import MarketingReportContent from '@/components/pages/report/marketing/MarketingReportContent'; +import MarketingReportContent from '@/components/pages/report/marketing/MarketingTabs'; const MarketingReportPage = () => { return ; diff --git a/src/components/pages/report/marketing/MarketingReportContent.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx similarity index 100% rename from src/components/pages/report/marketing/MarketingReportContent.tsx rename to src/components/pages/report/marketing/MarketingTabs.tsx diff --git a/src/components/pages/report/marketing/export/DailyMarketingPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx similarity index 100% rename from src/components/pages/report/marketing/export/DailyMarketingPDF.tsx rename to src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 9f1d6d42..cedf979f 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -16,7 +16,7 @@ import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingPDF'; +import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; import { Area } from '@/types/api/master-data/area'; import { From dbcf46912334db250a9baab4ca86c70f3b0f7021 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 13:44:22 +0700 Subject: [PATCH 076/149] refactor(FE): Remove DailyMarketingsTable component and refactor related files --- .../report/marketing/DailyMarketingsTable.tsx | 289 ---- .../pages/report/marketing/MarketingTabs.tsx | 2 +- .../marketing/tab/DailyMarketingTab.tsx | 1311 +++++++++++------ 3 files changed, 887 insertions(+), 715 deletions(-) delete mode 100644 src/components/pages/report/marketing/DailyMarketingsTable.tsx diff --git a/src/components/pages/report/marketing/DailyMarketingsTable.tsx b/src/components/pages/report/marketing/DailyMarketingsTable.tsx deleted file mode 100644 index 4904ef16..00000000 --- a/src/components/pages/report/marketing/DailyMarketingsTable.tsx +++ /dev/null @@ -1,289 +0,0 @@ -'use client'; - -import { ChangeEventHandler, useEffect, useState } from 'react'; -import useSWR from 'swr'; -import { ColumnDef, SortingState } from '@tanstack/react-table'; - -import { Icon } from '@iconify/react'; -import Table from '@/components/Table'; -import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; - -import { - cn, - formatCurrency, - formatDate, - formatNumber, - formatVechicleNumber, -} from '@/lib/helper'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { DailyMarketingRow } from '@/types/api/report/marketing'; -import { MarketingReportApi } from '@/services/api/report/marketing-report'; - -interface DailyMarketingsTableProps { - dailyMarketingsReportUrl: string; - onSetPage: (page: number) => void; - pageSize: number; - onSetPageSize: (pageSize: number) => void; - searchValue: string; - onSearchChange: ChangeEventHandler; - onFilterByChange: (filterBy: string) => void; - onSortByChange: (sort: 'asc' | 'desc' | '') => void; -} - -const DailyMarketingsTable = ({ - dailyMarketingsReportUrl, - onSetPage, - pageSize, - onSetPageSize, - searchValue, - onSearchChange, - onFilterByChange, - onSortByChange, -}: DailyMarketingsTableProps) => { - const { data: dailyMarketings, isLoading: isLoadingDailyMarketings } = useSWR( - dailyMarketingsReportUrl, - MarketingReportApi.getAllDailyMarketingFetcher, - { - keepPreviousData: true, - } - ); - - const [open, setOpen] = useState(true); - - const [sorting, setSorting] = useState([]); - - const dailyMarketingColumns: ColumnDef[] = [ - { - header: 'No', - cell: (props) => props.row.index + 1, - }, - { - accessorKey: 'so_date', - header: 'Tanggal Jual', - cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'), - footer: 'Total', - }, - { - accessorKey: 'realization_date', - header: 'Tanggal Realisasi', - cell: (props) => - formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'), - }, - { - accessorKey: 'aging_days', - header: 'Aging', - cell: (props) => `${props.row.original.aging_days} hari`, - }, - { - accessorKey: 'warehouse', - header: 'Gudang', - cell: ({ row }) => row.original.warehouse.name, - }, - { - accessorKey: 'customer', - header: 'Pelanggan', - cell: ({ row }) => row.original.customer.name, - }, - { - accessorKey: 'do_number', - header: 'No. DO', - enableSorting: false, - }, - { - accessorKey: 'sales_person', - header: 'Sales/Marketing', - cell: (props) => props.row.original.sales.name, - }, - { - accessorKey: 'vehicle_number', - header: 'No. Polisi', - cell: (props) => ( - - {formatVechicleNumber(props.row.original.vehicle_number)} - - ), - }, - { - accessorKey: 'marketing_type', - header: 'Marketing Type', - enableSorting: false, - }, - { - accessorKey: 'product', - header: 'Produk', - cell: ({ row }) => row.original.product.name, - }, - { - accessorKey: 'qty', - header: 'Kuantitas', - cell: (props) => formatNumber(props.row.original.qty), - footer: () => { - const totalQty = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_qty - : 0; - - return totalQty ? formatNumber(totalQty) : '-'; - }, - }, - { - accessorKey: 'average_weight', - header: 'Bobot Rata-Rata (Kg)', - cell: (props) => formatNumber(props.row.original.average_weight_kg), - footer: () => { - const totalAverageWeightKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.average_weight_kg - : 0; - - return totalAverageWeightKg ? formatNumber(totalAverageWeightKg) : '-'; - }, - }, - { - accessorKey: 'total_weight', - header: 'Bobot Total (Kg)', - cell: (props) => formatNumber(props.row.original.total_weight_kg), - footer: () => { - const totalWeightKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_weight_kg - : 0; - - return totalWeightKg ? formatNumber(totalWeightKg) : '-'; - }, - }, - { - accessorKey: 'sales_price', - header: 'Harga Jual (Rp)', - cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), - footer: () => { - const totalSalesPrice = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.average_sales_price - : 0; - - return totalSalesPrice ? formatNumber(totalSalesPrice) : '-'; - }, - }, - { - accessorKey: 'hpp_price', - header: 'HPP (Rp)', - cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), - footer: () => { - const totalHppPricePerKg = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_hpp_price_per_kg - : 0; - - return totalHppPricePerKg ? formatCurrency(totalHppPricePerKg) : '-'; - }, - }, - { - accessorKey: 'sales_amount', - header: 'Total (Rp)', - cell: (props) => formatCurrency(props.row.original.sales_amount), - footer: () => { - const totalSalesAmount = isResponseSuccess(dailyMarketings) - ? dailyMarketings?.total?.total_sales_amount - : 0; - - return totalSalesAmount ? formatCurrency(totalSalesAmount) : '-'; - }, - }, - ]; - - useEffect(() => { - if (sorting.length === 1) { - onFilterByChange(sorting[0].id); - onSortByChange(sorting[0].desc ? 'desc' : 'asc'); - } else { - onFilterByChange(''); - onSortByChange(''); - } - }, [sorting]); - - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(dailyMarketings) - ? dailyMarketings.data.length > 0 - : false - ); - } - }, [dailyMarketings, isResponseSuccess]); - - return ( - - -
Penjualan Harian
- - - - } - className='w-full!' - titleClassName='w-full p-0!' - > -
-
-
- -
-
- - - data={ - isResponseSuccess(dailyMarketings) ? dailyMarketings?.data : [] - } - columns={dailyMarketingColumns} - pageSize={pageSize} - onPageSizeChange={onSetPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(dailyMarketings) - ? dailyMarketings?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(dailyMarketings) - ? dailyMarketings?.meta?.total_results - : 0 - } - onPageChange={onSetPage} - isLoading={isLoadingDailyMarketings} - sorting={sorting} - setSorting={setSorting} - renderFooter={true} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(dailyMarketings) && - dailyMarketings?.data?.length === 0, - }), - }} - /> -
-
-
- ); -}; - -export default DailyMarketingsTable; diff --git a/src/components/pages/report/marketing/MarketingTabs.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx index 8a814689..de449f9c 100644 --- a/src/components/pages/report/marketing/MarketingTabs.tsx +++ b/src/components/pages/report/marketing/MarketingTabs.tsx @@ -14,7 +14,7 @@ const MarketingReportContent = () => { { id: '1', label: 'Penjualan Harian', - content: , + content: , }, { id: '2', diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index cedf979f..5fcc630f 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -1,472 +1,933 @@ -'use client'; - -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useState, useMemo, useCallback } from 'react'; +import useSWR from 'swr'; +import { useSelect } from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import { AreaApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; +import { WarehouseApi } from '@/services/api/master-data'; +import { CustomerApi } from '@/services/api/master-data'; +import { MarketingReportApi } from '@/services/api/report/marketing-report'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; +import { + formatCurrency, + formatNumber, + formatDate, + formatVechicleNumber, +} from '@/lib/helper'; +import { + DailyMarketingRow, + DailyMarketingReportResponse, +} from '@/types/api/report/marketing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import Button from '@/components/Button'; +import Dropdown from '@/components/Dropdown'; +import MenuItem from '@/components/menu/MenuItem'; +import Menu from '@/components/menu/Menu'; +import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; import { pdf } from '@react-pdf/renderer'; import toast from 'react-hot-toast'; - import { Icon } from '@iconify/react'; -import Button from '@/components/Button'; -import Dropdown from '@/components/dropdown/Dropdown'; -import DateInput from '@/components/input/DateInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; -import DailyMarketingsTable from '@/components/pages/report/marketing/DailyMarketingsTable'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; - -import { Area } from '@/types/api/master-data/area'; +import { useFormik } from 'formik'; import { - AreaApi, - CustomerApi, - LocationApi, - WarehouseApi, -} from '@/services/api/master-data'; -import { Warehouse } from '@/types/api/master-data/warehouse'; -import { Customer } from '@/types/api/master-data/customer'; -import { MarketingReportApi } from '@/services/api/report/marketing-report'; + DailyMarketingReportFilterSchema, + DailyMarketingReportFilterType, +} from '@/components/pages/report/marketing/filter/DailyMarketingFilter'; +import SelectInput from '@/components/input/SelectInput'; +import Modal, { useModal } from '@/components/Modal'; +import { cn } from '@/lib/helper'; +import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; +import { useEffect as useEffectHook } from 'react'; +import { httpClient } from '@/services/http/client'; +import { isResponseError } from '@/lib/api-helper'; import { MARKETING_DATE_FILTER_TYPE_OPTIONS, MARKETING_TYPE_OPTIONS, } from '@/config/constant'; -import { httpClient } from '@/services/http/client'; -import { BaseApiResponse } from '@/types/api/api-general'; -import { - DailyMarketingReport, - DailyMarketingReportResponse, -} from '@/types/api/report/marketing'; -import { isResponseError } from '@/lib/api-helper'; +import Badge from '@/components/Badge'; -const DailyMarketingReportContent = () => { - const { - state: tableFilterState, - updateFilter, - setPage, - setPageSize, - toQueryString: getTableFilterQueryString, - reset: resetFilter, - } = useTableFilter({ - initial: { - search: '', - area_id: '', - location_id: '', - warehouse_id: '', - customer_id: '', - start_date: '', - end_date: '', - marketing_type: '', - filter_by: '', - sort_by: '', +interface DailyMarketingTabProps { + tabId: string; +} + +interface FilterParams { + area_id?: string; + location_id?: string; + warehouse_id?: string; + customer_id?: string; + start_date?: string; + end_date?: string; + filter_by?: string; + marketing_type?: string; + sort_by?: string; +} + +const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + + // ===== SEARCH STATE ===== + const [searchValue, setSearchValue] = useState(''); + + // ===== FILTER STATE ===== + const [filterParams, setFilterParams] = useState({}); + + const filterModal = useModal(); + + // ===== OPTIONS ===== + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, + 'id', + 'name', + 'search' + ); + + const { options: locationOptions, isLoadingOptions: isLoadingLocations } = + useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { options: warehouseOptions, isLoadingOptions: isLoadingWarehouses } = + useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); + + const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = + useSelect(CustomerApi.basePath, 'id', 'name', 'search'); + + const handleFilterModalOpen = () => { + filterModal.openModal(); + formik.validateForm(); + }; + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + search: null, + area_id: null, + location_id: null, + warehouse_id: null, + customer_id: null, + start_date: null, + end_date: null, + filter_by: null, + marketing_type: null, + sort_by: null, }, - paramMap: { - page: 'page', - pageSize: 'limit', - area_id: 'area_id', - location_id: 'location_id', - warehouse_id: 'warehouse_id', - customer_id: 'customer_id', - start_date: 'start_date', - end_date: 'end_date', - marketing_type: 'marketing_type', - filter_by: 'filter_by', - sort_by: 'sort_by', + validationSchema: DailyMarketingReportFilterSchema, + onSubmit: (values, { setSubmitting }) => { + setFilterParams({ + area_id: values.area_id || undefined, + location_id: values.location_id || undefined, + warehouse_id: values.warehouse_id || undefined, + customer_id: values.customer_id || undefined, + start_date: values.start_date || undefined, + end_date: values.end_date || undefined, + filter_by: values.filter_by || undefined, + marketing_type: values.marketing_type || undefined, + sort_by: values.sort_by || undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setSubmitting(false); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); }, }); - const dailyMarketingsReportUrl = `${MarketingReportApi.basePath}${getTableFilterQueryString()}`; - - const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = - useState(false); - const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false); - - const [selectedArea, setSelectedArea] = useState(null); - const { - setInputValue: setAreaInputValue, - options: areaOptions, - isLoadingOptions: isLoadingAreaOptions, - loadMore: loadMoreAreas, - } = useSelect(AreaApi.basePath, 'id', 'name'); - - const areaChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedArea(val as OptionType); - updateFilter('area_id', val ? ((val as OptionType).value as string) : ''); - }; - - const [selectedLocation, setSelectedLocation] = useState( - null + // ===== SEARCH CHANGE HANDLER ===== + const searchChangeHandler = useCallback( + (e: React.ChangeEvent) => { + setSearchValue(e.target.value); + }, + [] ); - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name'); - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - updateFilter( - 'location_id', - val ? ((val as OptionType).value as string) : '' + // ===== DERIVED VALUES ===== + 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 [selectedWarehouse, setSelectedWarehouse] = useState( - null + 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 warehouseValue = useMemo(() => { + if (!formik.values.warehouse_id) return null; + return ( + warehouseOptions.find( + (opt) => String(opt.value) === formik.values.warehouse_id + ) || null + ); + }, [formik.values.warehouse_id, warehouseOptions]); + + const customerValue = useMemo(() => { + if (!formik.values.customer_id) return null; + return ( + customerOptions.find( + (opt) => String(opt.value) === formik.values.customer_id + ) || null + ); + }, [formik.values.customer_id, customerOptions]); + + const filterByValue = useMemo(() => { + if (!formik.values.filter_by) return null; + return ( + MARKETING_DATE_FILTER_TYPE_OPTIONS.find( + (opt) => opt.value === formik.values.filter_by + ) || null + ); + }, [formik.values.filter_by]); + + const marketingTypeValue = useMemo(() => { + if (!formik.values.marketing_type) return null; + return ( + MARKETING_TYPE_OPTIONS.find( + (opt) => opt.value === formik.values.marketing_type + ) || null + ); + }, [formik.values.marketing_type]); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (filterParams.area_id) { + count += 1; + } + + if (filterParams.location_id) { + count += 1; + } + + if (filterParams.warehouse_id) { + count += 1; + } + + if (filterParams.customer_id) { + count += 1; + } + + if (filterParams.start_date || filterParams.end_date) { + count += 1; + } + + if (filterParams.filter_by) { + count += 1; + } + + if (filterParams.marketing_type) { + count += 1; + } + + if (filterParams.sort_by) { + count += 1; + } + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: dailyMarketings, isLoading } = useSWR( + isSubmitted + ? () => { + const params = new URLSearchParams(); + + if (searchValue) params.set('search', searchValue); + if (filterParams.area_id) params.set('area_id', filterParams.area_id); + if (filterParams.location_id) + params.set('location_id', filterParams.location_id); + if (filterParams.warehouse_id) + params.set('warehouse_id', filterParams.warehouse_id); + if (filterParams.customer_id) + params.set('customer_id', filterParams.customer_id); + if (filterParams.start_date) + params.set('start_date', filterParams.start_date); + if (filterParams.end_date) + params.set('end_date', filterParams.end_date); + if (filterParams.filter_by) + params.set('filter_by', filterParams.filter_by); + if (filterParams.marketing_type) + params.set('marketing_type', filterParams.marketing_type); + if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by); + + return ['daily-marketing-report', params.toString()]; + } + : null, + ([, params]) => + MarketingReportApi.getAllDailyMarketingFetcher( + `${MarketingReportApi.basePath}?${params}` + ) ); - const { - setInputValue: setWarehouseInputValue, - options: warehouseOptions, - isLoadingOptions: isLoadingWarehouseOptions, - loadMore: loadMoreWarehouses, - } = useSelect(WarehouseApi.basePath, 'id', 'name'); - const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedWarehouse(val as OptionType); - updateFilter( - 'warehouse_id', - val ? ((val as OptionType).value as string) : '' - ); - }; - - const [selectedCustomer, setSelectedCustomer] = useState( - null + const data: DailyMarketingRow[] = useMemo( + () => + isResponseSuccess(dailyMarketings) + ? (dailyMarketings?.data as DailyMarketingRow[]) || [] + : [], + [dailyMarketings] ); - const { - setInputValue: setCustomerInputValue, - options: customerOptions, - isLoadingOptions: isLoadingCustomerOptions, - loadMore: loadMoreCustomers, - } = useSelect(CustomerApi.basePath, 'id', 'name'); - const customerChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedCustomer(val as OptionType); - updateFilter( - 'customer_id', - val ? ((val as OptionType).value as string) : '' - ); - }; + const summaryTotal = useMemo( + () => + isResponseSuccess(dailyMarketings) && dailyMarketings?.total + ? dailyMarketings.total + : undefined, + [dailyMarketings] + ); - const startDateChangeHandler = (e: React.ChangeEvent) => { - updateFilter('start_date', e.target.value ? e.target.value : ''); - }; - - const endDateChangeHandler = (e: React.ChangeEvent) => { - updateFilter('end_date', e.target.value ? e.target.value : ''); - }; - - const [selectedMarketingDateFilterType, setSelectedMarketingDateFilterType] = - useState(null); - const marketingDateFilterTypeChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - setSelectedMarketingDateFilterType(val as OptionType); - updateFilter('filter_by', val ? ((val as OptionType).value as string) : ''); - }; - - const [selectedMarketingType, setSelectedMarketingType] = - useState(null); - const marketingTypeChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - setSelectedMarketingType(val as OptionType); - updateFilter( - 'marketing_type', - val ? ((val as OptionType).value as string) : '' - ); - }; - - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; - - const filterByChangeHandler = (filterBy: string) => { - updateFilter('filter_by', filterBy); - }; - - const sortByChangeHandler = (sort: 'asc' | 'desc' | '') => { - updateFilter('sort_by', sort); - }; - - const exportToExcelHandler = async () => { - setIsLoadingExportingToExcel(true); - - await MarketingReportApi.exportDailyMarketingToExcel( - getTableFilterQueryString() - ); - - setIsLoadingExportingToExcel(false); - }; - - const exportToPdfHandler = async () => { - setIsLoadingExportingToPdf(true); - - const params = new URLSearchParams(getTableFilterQueryString()); + // ===== EXPORT DATA FETCHER ===== + const dailyMarketingsExport = useCallback(async (): Promise< + DailyMarketingRow[] | null + > => { + const params = new URLSearchParams(); + if (searchValue) params.set('search', searchValue); + if (filterParams.area_id) params.set('area_id', filterParams.area_id); + if (filterParams.location_id) + params.set('location_id', filterParams.location_id); + if (filterParams.warehouse_id) + params.set('warehouse_id', filterParams.warehouse_id); + if (filterParams.customer_id) + params.set('customer_id', filterParams.customer_id); + if (filterParams.start_date) + params.set('start_date', filterParams.start_date); + if (filterParams.end_date) params.set('end_date', filterParams.end_date); + if (filterParams.filter_by) params.set('filter_by', filterParams.filter_by); + if (filterParams.marketing_type) + params.set('marketing_type', filterParams.marketing_type); + if (filterParams.sort_by) params.set('sort_by', filterParams.sort_by); params.set('limit', '9999999'); const queryString = `?${params.toString()}`; try { - const dailyMarketingsReport = - await httpClient( - `${MarketingReportApi.basePath}${queryString}` - ); + const response = await httpClient( + `${MarketingReportApi.basePath}${queryString}` + ); - if (isResponseError(dailyMarketingsReport)) { - toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + if (isResponseError(response)) { + return null; + } + + return response.data || []; + } catch { + return null; + } + }, [filterParams, searchValue]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const queryString = new URLSearchParams(); + + if (searchValue) queryString.set('search', searchValue); + if (filterParams.area_id) + queryString.set('area_id', filterParams.area_id); + if (filterParams.location_id) + queryString.set('location_id', filterParams.location_id); + if (filterParams.warehouse_id) + queryString.set('warehouse_id', filterParams.warehouse_id); + if (filterParams.customer_id) + queryString.set('customer_id', filterParams.customer_id); + if (filterParams.start_date) + queryString.set('start_date', filterParams.start_date); + if (filterParams.end_date) + queryString.set('end_date', filterParams.end_date); + if (filterParams.filter_by) + queryString.set('filter_by', filterParams.filter_by); + if (filterParams.marketing_type) + queryString.set('marketing_type', filterParams.marketing_type); + if (filterParams.sort_by) + queryString.set('sort_by', filterParams.sort_by); + + await MarketingReportApi.exportDailyMarketingToExcel( + `?${queryString.toString()}` + ); + + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [filterParams, searchValue]); + + const handleExportPDF = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await dailyMarketingsExport(); + + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); return; } - const openPdf = async () => { - const dailyMarketingReportPdfBlob = await pdf( - - ).toBlob(); + const dailyMarketingReportPdfBlob = await pdf( + + ).toBlob(); - const dailyMarketingReportPdfUrl = URL.createObjectURL( - dailyMarketingReportPdfBlob - ); - window.open(dailyMarketingReportPdfUrl, '_blank'); - }; + const dailyMarketingReportPdfUrl = URL.createObjectURL( + dailyMarketingReportPdfBlob + ); + window.open(dailyMarketingReportPdfUrl, '_blank'); - const downloadPdf = async () => { - const blob = await pdf( - - ).toBlob(); - const url = URL.createObjectURL(blob); - - const link = document.createElement('a'); - link.href = url; - link.download = 'laporan-penjualan-harian.pdf'; - link.click(); - - URL.revokeObjectURL(url); - }; - - await openPdf(); - } catch (error) { - toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + toast.success('PDF berhasil dibuat.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); } + }, [dailyMarketingsExport, summaryTotal]); - setIsLoadingExportingToPdf(false); + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useMarketingTabStore((state) => state.setTabActions); + const clearTabActions = useMarketingTabStore( + (state) => state.clearTabActions + ); + + useEffectHook(() => { + setTabActions( + tabId, +
+ + } + className={{ + wrapper: 'w-full min-w-48 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> + + + + +
+ + + Export + +
+ + +
+ + } + > + + + +
+ ); + }, [ + tabId, + searchValue, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + filterModal.open, + setTabActions, + ]); + + useEffectHook(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + const getTableColumns = (): ColumnDef[] => { + const tableColumns: ColumnDef[] = [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + footer: () =>
TOTAL
, + }, + { + id: 'so_date', + header: 'Tanggal Jual', + accessorKey: 'so_date', + cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'), + footer: () =>
ALL
, + }, + { + id: 'realization_date', + header: 'Tanggal Realisasi', + accessorKey: 'realization_date', + cell: (props) => + formatDate(props.row.original.realization_date, 'DD-MMM-YYYY'), + footer: () =>
-
, + }, + { + id: 'aging_days', + header: 'Aging', + accessorKey: 'aging_days', + cell: (props) => `${props.row.original.aging_days} hari`, + footer: () =>
-
, + }, + { + id: 'warehouse', + header: 'Gudang', + accessorKey: 'warehouse', + cell: ({ row }) => row.original.warehouse.name, + footer: () =>
-
, + }, + { + id: 'customer', + header: 'Pelanggan', + accessorKey: 'customer', + cell: ({ row }) => row.original.customer.name, + footer: () =>
-
, + }, + { + id: 'do_number', + header: 'No. DO', + accessorKey: 'do_number', + footer: () =>
-
, + }, + { + id: 'sales_person', + header: 'Sales/Marketing', + accessorKey: 'sales', + cell: (props) => props.row.original.sales.name, + footer: () =>
-
, + }, + { + id: 'vehicle_number', + header: 'No. Polisi', + accessorKey: 'vehicle_number', + cell: (props) => ( + + {formatVechicleNumber(props.row.original.vehicle_number)} + + ), + footer: () =>
-
, + }, + { + id: 'marketing_type', + header: 'Marketing Type', + accessorKey: 'marketing_type', + footer: () =>
-
, + }, + { + id: 'product', + header: 'Produk', + accessorKey: 'product', + cell: ({ row }) => row.original.product.name, + footer: () =>
-
, + }, + { + id: 'qty', + header: 'Kuantitas', + accessorKey: 'qty', + cell: (props) => formatNumber(props.row.original.qty), + footer: () => ( +
+ {summaryTotal?.total_qty + ? formatNumber(summaryTotal.total_qty) + : '-'} +
+ ), + }, + { + id: 'average_weight', + header: 'Bobot Rata-Rata (Kg)', + accessorKey: 'average_weight_kg', + cell: (props) => formatNumber(props.row.original.average_weight_kg), + footer: () => ( +
+ {summaryTotal?.average_weight_kg + ? formatNumber(summaryTotal.average_weight_kg) + : '-'} +
+ ), + }, + { + id: 'total_weight', + header: 'Bobot Total (Kg)', + accessorKey: 'total_weight_kg', + cell: (props) => formatNumber(props.row.original.total_weight_kg), + footer: () => ( +
+ {summaryTotal?.total_weight_kg + ? formatNumber(summaryTotal.total_weight_kg) + : '-'} +
+ ), + }, + { + id: 'sales_price', + header: 'Harga Jual (Rp)', + accessorKey: 'sales_price_per_kg', + cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), + footer: () => ( +
+ {summaryTotal?.average_sales_price + ? formatNumber(summaryTotal.average_sales_price) + : '-'} +
+ ), + }, + { + id: 'hpp_price', + header: 'HPP (Rp)', + accessorKey: 'hpp_price_per_kg', + cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), + footer: () => ( +
+ {summaryTotal?.total_hpp_price_per_kg + ? formatCurrency(summaryTotal.total_hpp_price_per_kg) + : '-'} +
+ ), + }, + { + id: 'sales_amount', + header: 'Total (Rp)', + accessorKey: 'sales_amount', + cell: (props) => formatCurrency(props.row.original.sales_amount), + footer: () => ( +
+ {summaryTotal?.total_sales_amount + ? formatCurrency(summaryTotal.total_sales_amount) + : '-'} +
+ ), + }, + ]; + return tableColumns; }; - const handleReset = () => { - setSelectedArea(null); - setSelectedLocation(null); - setSelectedWarehouse(null); - setSelectedCustomer(null); - setSelectedMarketingType(null); - resetFilter(); - }; - - useEffect(() => { - if ( - tableFilterState.filter_by === 'realization_date' || - tableFilterState.filter_by === 'so_date' - ) { - setSelectedMarketingDateFilterType({ - label: - tableFilterState.filter_by === 'realization_date' - ? 'Tanggal Realisasi' - : 'Tanggal SO', - value: tableFilterState.filter_by, - }); - } else { - setSelectedMarketingDateFilterType(null); - } - }, [tableFilterState.filter_by]); - return ( -
-
-

Penjualan Harian

+ <> +
+ {!isSubmitted ? ( + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( + + } + title='Memuat Data Penjualan Harian' + subtitle='Silakan tunggu sebentar...' + /> + ) : data.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( +
0} + className={{ + containerClassName: cn('p-3', { + 'w-full mb-20': data.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> + )} - {/* Filters */} -
-
- - - - - - - - - - - -
- -
- - - - -
- - - - - - Export{' '} - - - } - > - - - - - + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
-
+
+
+ {/* Area Filter */} + { + formik.setFieldValue( + 'area_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> - -
+ {/* Location Filter */} + { + formik.setFieldValue( + 'location_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Warehouse Filter */} + { + formik.setFieldValue( + 'warehouse_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Customer Filter */} + { + formik.setFieldValue( + 'customer_id', + val && !Array.isArray(val) ? String(val.value) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Date Range Filter */} +
+ +
+ { + formik.setFieldValue('start_date', e.target.value || null); + }} + className={{ wrapper: 'w-full' }} + isError={ + !!formik.errors.start_date && formik.touched.start_date + } + /> + {formik.errors.start_date && formik.touched.start_date && ( +
+ {formik.errors.start_date} +
+ )} + { + formik.setFieldValue('end_date', e.target.value || null); + }} + className={{ wrapper: 'w-full' }} + isError={!!formik.errors.end_date && formik.touched.end_date} + /> + {formik.errors.end_date && formik.touched.end_date && ( +
+ {formik.errors.end_date} +
+ )} +
+
+ + {/* Filter By Date Type */} + { + formik.setFieldValue( + 'filter_by', + val && !Array.isArray(val) ? (val.value as string) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + {/* Marketing Type Filter */} + { + formik.setFieldValue( + 'marketing_type', + val && !Array.isArray(val) ? (val.value as string) : null + ); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> +
+ + {/* Modal Footer */} +
+ + +
+ + + ); }; -export default DailyMarketingReportContent; +export default DailyMarketingTab; From 510573e66f16c1f2dd3459e5343755cbeecb962b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 13:55:31 +0700 Subject: [PATCH 077/149] refactor(FE): Add Excel export functionality for daily marketing report --- .../export/DailyMarketingExportXLSX.tsx | 118 ++++++++++++++++++ .../marketing/tab/DailyMarketingTab.tsx | 49 ++++---- 2 files changed, 141 insertions(+), 26 deletions(-) diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx index e69de29b..8c368fbf 100644 --- a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx +++ b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx @@ -0,0 +1,118 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { + formatCurrency, + formatNumber, + formatDate, + formatVechicleNumber, +} from '@/lib/helper'; +import { DailyMarketingRow, SalesSummary } from '@/types/api/report/marketing'; + +interface DailyMarketingExportExcelParams { + data: DailyMarketingRow[]; + summaryTotal?: SalesSummary; + period?: string; +} + +export const generateDailyMarketingExcel = async ( + params: DailyMarketingExportExcelParams +): Promise => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + // ===== DAILY MARKETING WORKSHEET ===== + const columns = [ + { header: 'No', key: 'no', width: 5 }, + { header: 'Tanggal Jual', key: 'soDate', width: 15 }, + { header: 'Tanggal Realisasi', key: 'realizationDate', width: 18 }, + { header: 'Aging', key: 'aging', width: 10 }, + { header: 'Gudang', key: 'warehouse', width: 25 }, + { header: 'Pelanggan', key: 'customer', width: 25 }, + { header: 'No. DO', key: 'doNumber', width: 15 }, + { header: 'Sales/Marketing', key: 'sales', width: 20 }, + { header: 'No. Polisi', key: 'vehicleNumber', width: 15 }, + { header: 'Marketing Type', key: 'marketingType', width: 15 }, + { header: 'Produk', key: 'product', width: 20 }, + { header: 'Kuantitas', key: 'qty', width: 12 }, + { header: 'Bobot Rata-Rata (Kg)', key: 'averageWeight', width: 20 }, + { header: 'Bobot Total (Kg)', key: 'totalWeight', width: 18 }, + { header: 'Harga Jual (Rp)', key: 'salesPrice', width: 18 }, + { header: 'HPP (Rp)', key: 'hppPrice', width: 15 }, + { header: 'Total (Rp)', key: 'salesAmount', width: 20 }, + ]; + + const worksheet = workbook.addWorksheet('Laporan Marketing Harian'); + worksheet.columns = columns; + + // Add data rows + params.data.forEach((item: DailyMarketingRow, index: number) => { + worksheet.addRow({ + no: index + 1, + soDate: formatDate(item.so_date, 'DD-MMM-YYYY'), + realizationDate: formatDate(item.realization_date, 'DD-MMM-YYYY'), + aging: `${item.aging_days} hari`, + warehouse: item.warehouse?.name || '', + customer: item.customer?.name || '', + doNumber: item.do_number || '', + sales: item.sales?.name || '', + vehicleNumber: formatVechicleNumber(item.vehicle_number), + marketingType: item.marketing_type || '', + product: item.product?.name || '', + qty: formatNumber(item.qty || 0), + averageWeight: formatNumber(item.average_weight_kg || 0), + totalWeight: formatNumber(item.total_weight_kg || 0), + salesPrice: formatCurrency(item.sales_price_per_kg || 0), + hppPrice: formatCurrency(item.hpp_price_per_kg || 0), + salesAmount: formatCurrency(item.sales_amount || 0), + }); + }); + + // Add TOTAL row if summary data is available + if (params.summaryTotal) { + worksheet.addRow({ + no: 'TOTAL', + soDate: 'ALL', + realizationDate: '-', + aging: '-', + warehouse: '-', + customer: '-', + doNumber: '-', + sales: '-', + vehicleNumber: '-', + marketingType: '-', + product: '-', + qty: formatNumber(params.summaryTotal.total_qty || 0), + averageWeight: formatNumber(params.summaryTotal.average_weight_kg || 0), + totalWeight: formatNumber(params.summaryTotal.total_weight_kg || 0), + salesPrice: formatNumber(params.summaryTotal.average_sales_price || 0), + hppPrice: formatCurrency(params.summaryTotal.total_hpp_price_per_kg || 0), + salesAmount: formatCurrency(params.summaryTotal.total_sales_amount || 0), + }); + } + + worksheet.columns.forEach((column) => { + if (column.width && column.width < 10) { + column.width = 10; + } + }); + + const currentDate = new Date().toISOString().split('T')[0]; + const filename = params.period + ? `laporan-marketing-harian-${params.period}.xlsx` + : `laporan-marketing-harian-${currentDate}.xlsx`; + + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); +}; diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 5fcc630f..a687fdf5 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -15,6 +15,7 @@ import { formatNumber, formatDate, formatVechicleNumber, + formatTitleCase, } from '@/lib/helper'; import { DailyMarketingRow, @@ -23,9 +24,8 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; import DailyMarketingReportPDF from '@/components/pages/report/marketing/export/DailyMarketingExportPDF'; +import { generateDailyMarketingExcel } from '@/components/pages/report/marketing/export/DailyMarketingExportXLSX'; import { pdf } from '@react-pdf/renderer'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -336,31 +336,23 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { - const queryString = new URLSearchParams(); + const allDataForExport = await dailyMarketingsExport(); - if (searchValue) queryString.set('search', searchValue); - if (filterParams.area_id) - queryString.set('area_id', filterParams.area_id); - if (filterParams.location_id) - queryString.set('location_id', filterParams.location_id); - if (filterParams.warehouse_id) - queryString.set('warehouse_id', filterParams.warehouse_id); - if (filterParams.customer_id) - queryString.set('customer_id', filterParams.customer_id); - if (filterParams.start_date) - queryString.set('start_date', filterParams.start_date); - if (filterParams.end_date) - queryString.set('end_date', filterParams.end_date); - if (filterParams.filter_by) - queryString.set('filter_by', filterParams.filter_by); - if (filterParams.marketing_type) - queryString.set('marketing_type', filterParams.marketing_type); - if (filterParams.sort_by) - queryString.set('sort_by', filterParams.sort_by); + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } - await MarketingReportApi.exportDailyMarketingToExcel( - `?${queryString.toString()}` - ); + const period = + filterParams.start_date && filterParams.end_date + ? `${formatDate(filterParams.start_date, 'DD-MMM-YYYY')}_to_${formatDate(filterParams.end_date, 'DD-MMM-YYYY')}` + : undefined; + + await generateDailyMarketingExcel({ + data: allDataForExport, + summaryTotal: summaryTotal, + period: period, + }); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { @@ -368,7 +360,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { } finally { setIsExcelExportLoading(false); } - }, [filterParams, searchValue]); + }, [filterParams, dailyMarketingsExport, summaryTotal]); const handleExportPDF = useCallback(async () => { setIsPdfExportLoading(true); @@ -588,6 +580,11 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { id: 'marketing_type', header: 'Marketing Type', accessorKey: 'marketing_type', + cell: (props) => ( + + {formatTitleCase(props.row.original.marketing_type || '-')} + + ), footer: () =>
-
, }, { From b154b478bcb477ee8b77e4ec9ea86115d7dd2798 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 14:15:56 +0700 Subject: [PATCH 078/149] refactor(FE): Refactor production result components structure --- src/app/report/production-result/page.tsx | 2 +- .../ProductionResultExportPDF.tsx} | 0 .../production-result/export/ProductionResultExportXLSX.tsx | 0 .../report/production-result/filter/ProductionResultFilter.ts | 0 .../production-result/skeleton/ProductionResultSkeleton.tsx | 0 .../ProductionResultTab.tsx} | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) rename src/components/pages/report/production-result/{ProductionResultReportPDF.tsx => export/ProductionResultExportPDF.tsx} (100%) create mode 100644 src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx create mode 100644 src/components/pages/report/production-result/filter/ProductionResultFilter.ts create mode 100644 src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx rename src/components/pages/report/production-result/{ProductionResultContent.tsx => tab/ProductionResultTab.tsx} (99%) diff --git a/src/app/report/production-result/page.tsx b/src/app/report/production-result/page.tsx index cdac598c..fb8f2a0c 100644 --- a/src/app/report/production-result/page.tsx +++ b/src/app/report/production-result/page.tsx @@ -1,4 +1,4 @@ -import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent'; +import ProductionResultContent from '@/components/pages/report/production-result/tab/ProductionResultTab'; const ProductionResultReportPage = () => { return ( diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx similarity index 100% rename from src/components/pages/report/production-result/ProductionResultReportPDF.tsx rename to src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx diff --git a/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx b/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/report/production-result/ProductionResultContent.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx similarity index 99% rename from src/components/pages/report/production-result/ProductionResultContent.tsx rename to src/components/pages/report/production-result/tab/ProductionResultTab.tsx index d79d4c94..84b21c41 100644 --- a/src/components/pages/report/production-result/ProductionResultContent.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -31,7 +31,7 @@ import { ProductionResultReportApi } from '@/services/api/report/production-resu import { BaseApiResponse } from '@/types/api/api-general'; import { httpClient } from '@/services/http/client'; import { ProductionResult } from '@/types/api/report/production-result'; -import ProductionResultReportPDF from './ProductionResultReportPDF'; +import ProductionResultReportPDF from '../export/ProductionResultExportPDF'; import { pdf } from '@react-pdf/renderer'; const ProductionResultContent = () => { From dc4e945a35946accb5e3115640870962287ec91d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 14:50:19 +0700 Subject: [PATCH 079/149] refactor(FE): Refactor table column definitions to remove unused row parameter --- .../export/ProductionResultExportPDF.tsx | 14 ++-- .../filter/ProductionResultFilter.ts | 50 ++++++++++++ .../skeleton/ProductionResultSkeleton.tsx | 38 +++++++++ .../tab/ProductionResultTab.tsx | 79 +++++++++++++++++-- 4 files changed, 169 insertions(+), 12 deletions(-) diff --git a/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx b/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx index eabb03bf..76336569 100644 --- a/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx +++ b/src/components/pages/report/production-result/export/ProductionResultExportPDF.tsx @@ -66,7 +66,7 @@ const getBwTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'woa', @@ -114,7 +114,7 @@ const getDepTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'dep_kum', @@ -141,7 +141,7 @@ const getButiranTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'butiran_utuh', @@ -196,7 +196,7 @@ const getKgTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'kg_utuh', @@ -251,7 +251,7 @@ const getPersenTableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'persen_utuh', @@ -292,7 +292,7 @@ const getProduksi1TableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'hd', @@ -361,7 +361,7 @@ const getProduksi2TableColumns = (): PdfColumn[] => [ header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'fcr', diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts index e69de29b..1c1979eb 100644 --- a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts +++ b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts @@ -0,0 +1,50 @@ +import * as yup from 'yup'; + +export type ProductionResultFilterType = { + area_id: string | null; + location_id: string | null; + project_flock_id: string | null; + kandang_id: string | null; + date_start: string | null; + date_end: string | null; + sort_by: string | null; + show_unrecorded: boolean | null; +}; + +export const ProductionResultFilterSchema = yup.object({ + area_id: yup.string().nullable(), + location_id: yup.string().nullable(), + project_flock_id: yup.string().nullable(), + kandang_id: yup.string().nullable(), + date_start: yup + .string() + .nullable() + .test( + 'is-valid-date', + 'Tanggal mulai tidak valid', + (value) => !value || !isNaN(Date.parse(value)) + ), + date_end: yup + .string() + .nullable() + .test( + 'is-valid-date', + 'Tanggal akhir tidak valid', + (value) => !value || !isNaN(Date.parse(value)) + ) + .test( + 'is-after-start', + 'Tanggal akhir tidak boleh lebih awal dari tanggal mulai', + function (value) { + const { date_start } = this.parent; + if (!date_start || !value) return true; + return new Date(value) >= new Date(date_start); + } + ), + sort_by: yup.string().nullable(), + show_unrecorded: yup.boolean().nullable(), +}); + +export type ProductionResultFilterValues = yup.InferType< + typeof ProductionResultFilterSchema +>; diff --git a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx index e69de29b..c8cd63d8 100644 --- a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx +++ b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ProductionResult } from '@/types/api/report/production-result'; +import { ColumnDef } from '@tanstack/react-table'; + +const ProductionResultSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ProductionResultSkeleton; diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx index 84b21c41..ac5e76fd 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -1,6 +1,7 @@ 'use client'; -import { useState } from 'react'; +import React, { useState } from 'react'; +import { generateProductionResultExcel } from '../export/ProductionResultExportXLSX'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -161,11 +162,79 @@ const ProductionResultContent = () => { const exportToExcelHandler = async () => { setIsLoadingExportingToExcel(true); - await ProductionResultReportApi.exportProductionResultToExcel( - projectFlockKandangs - ); + try { + let projectFlockKandangsData: BaseProjectFlockKandang[] = []; - setIsLoadingExportingToExcel(false); + if (selectedProjectFlockKandang) { + const projectFlockKandangResponse = + await ProjectFlockKandangApi.getSingle( + selectedProjectFlockKandang?.value as number + ); + + projectFlockKandangsData = isResponseSuccess( + projectFlockKandangResponse + ) + ? [projectFlockKandangResponse.data] + : []; + } else { + const projectFlockKandangsResponse = + await ProjectFlockKandangApi.getAll({ + area_id: selectedArea?.value, + project_flock_id: selectedProjectFlock?.value, + }); + + projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsResponse + ) + ? projectFlockKandangsResponse.data + : []; + } + + const productionResultData: ProductionResult[] = []; + + for (const kandang of projectFlockKandangsData) { + const getProductionResultPath = `${ProductionResultReportApi.basePath}/${kandang.id}?page=1&limit=100`; + const getProductionResultRes = await httpClient< + BaseApiResponse + >(getProductionResultPath); + + if (isResponseSuccess(getProductionResultRes)) { + productionResultData.push( + ...(getProductionResultRes.data?.map((result) => ({ + ...result, + project_flock: { + ...result.project_flock, + name: + selectedProjectFlock?.label || + kandang.project_flock?.name || + `Project Flock #${kandang.project_flock_id}`, + category: kandang.project_flock?.category || '', + kandang: { + ...result.project_flock?.kandang, + name: + kandang.kandang?.name || `Kandang #${kandang.kandang_id}`, + }, + }, + })) || []) + ); + } + } + + if (productionResultData.length === 0) { + toast.error('Tidak ada data untuk diexport.'); + setIsLoadingExportingToExcel(false); + return; + } + + await generateProductionResultExcel({ + data: productionResultData, + period: '', + }); + } catch { + toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); + } finally { + setIsLoadingExportingToExcel(false); + } }; const exportToPdfHandler = async () => { From 6595ff7a6ec7436bce4f829f7cfed37715fee4b2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:03:24 +0700 Subject: [PATCH 080/149] refactor(FE): Add function to export production results to Excel --- .../export/ProductionResultExportXLSX.tsx | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx b/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx index e69de29b..af0380c0 100644 --- a/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx +++ b/src/components/pages/report/production-result/export/ProductionResultExportXLSX.tsx @@ -0,0 +1,135 @@ +'use client'; + +import ExcelJS from 'exceljs'; +import { formatNumber } from '@/lib/helper'; +import { ProductionResult } from '@/types/api/report/production-result'; + +interface ProductionResultExportExcelParams { + data: ProductionResult[]; + period?: string; +} + +export const generateProductionResultExcel = async ( + params: ProductionResultExportExcelParams +): Promise => { + if (!params.data || params.data.length === 0) { + return; + } + + const workbook = new ExcelJS.Workbook(); + + // ===== PRODUCTION RESULT WORKSHEET ===== + const columns = [ + { header: 'No', key: 'no', width: 6 }, + { header: 'Project Flock', key: 'projectFlockName', width: 25 }, + { + header: 'Category', + key: 'projectFlockCategory', + width: 18, + }, + { header: 'Kandang', key: 'kandangName', width: 18 }, + { header: 'Week of Age (WOA)', key: 'woa', width: 20 }, + { header: 'Body Weight (BW)', key: 'bw', width: 18 }, + { header: 'Body Weight (Std BW)', key: 'stdBw', width: 22 }, + { header: 'Uniformity (%)', key: 'uniformity', width: 16 }, + { header: 'Uniformity Std (%)', key: 'stdUniformity', width: 20 }, + { header: 'Depletion Cumulative', key: 'depKum', width: 22 }, + { header: 'Depletion Standard', key: 'depStd', width: 20 }, + { header: 'Telur Utuh', key: 'butiranUtuh', width: 14 }, + { header: 'Telur Putih', key: 'butiranPutih', width: 14 }, + { header: 'Telur Retak', key: 'butiranRetak', width: 14 }, + { header: 'Telur Pecah', key: 'butiranPecah', width: 14 }, + { header: 'Jumlah Telur', key: 'butiranJumlah', width: 16 }, + { header: 'Total Telur', key: 'totalButir', width: 14 }, + { header: 'Utuh (Kg)', key: 'kgUtuh', width: 12 }, + { header: 'Putih (Kg)', key: 'kgPutih', width: 12 }, + { header: 'Retak (Kg)', key: 'kgRetak', width: 12 }, + { header: 'Pecah (Kg)', key: 'kgPecah', width: 12 }, + { header: 'Jumlah (Kg)', key: 'kgJumlah', width: 14 }, + { header: 'Total Weight (Kg)', key: 'totalKg', width: 20 }, + { header: 'Utuh (%)', key: 'persenUtuh', width: 12 }, + { header: 'Putih (%)', key: 'persenPutih', width: 12 }, + { header: 'Retak (%)', key: 'persenRetak', width: 12 }, + { header: 'Pecah (%)', key: 'persenPecah', width: 12 }, + { header: 'Hen Day (HD)', key: 'hd', width: 15 }, + { header: 'Hen Day Std (HD Std)', key: 'hdStd', width: 22 }, + { header: 'Feed Intake (FI)', key: 'fi', width: 18 }, + { header: 'Feed Intake Std (FI Std)', key: 'fiStd', width: 25 }, + { header: 'Egg Mass (EM)', key: 'em', width: 16 }, + { header: 'Egg Mass Std (EM Std)', key: 'emStd', width: 23 }, + { header: 'Egg Weight (EW)', key: 'ew', width: 18 }, + { header: 'Egg Weight Std (EW Std)', key: 'ewStd', width: 25 }, + { header: 'Feed Conversion Ratio (FCR)', key: 'fcr', width: 30 }, + { + header: 'Feed Conversion Ratio Std (FCR Std)', + key: 'fcrStd', + width: 35, + }, + { header: 'Hen House (HH)', key: 'hh', width: 18 }, + { header: 'Hen House Std (HH Std)', key: 'hhStd', width: 25 }, + ]; + + const worksheet = workbook.addWorksheet('Production Result'); + worksheet.columns = columns; + + // Add data rows + params.data.forEach((item: ProductionResult, index: number) => { + worksheet.addRow({ + no: index + 1, + projectFlockName: item.project_flock?.name || '', + projectFlockCategory: item.project_flock?.category || '', + kandangName: item.project_flock?.kandang?.name || '', + woa: formatNumber(item.woa || 0), + bw: formatNumber(item.bw || 0), + stdBw: formatNumber(item.std_bw || 0), + uniformity: formatNumber(item.uniformity || 0), + stdUniformity: item.std_uniformity || '', + depKum: formatNumber(item.dep_kum || 0), + depStd: formatNumber(item.dep_std || 0), + butiranUtuh: formatNumber(item.butiran_utuh || 0), + butiranPutih: formatNumber(item.butiran_putih || 0), + butiranRetak: formatNumber(item.butiran_retak || 0), + butiranPecah: formatNumber(item.butiran_pecah || 0), + butiranJumlah: formatNumber(item.butiran_jumlah || 0), + totalButir: formatNumber(item.total_butir || 0), + kgUtuh: formatNumber(item.kg_utuh || 0), + kgPutih: formatNumber(item.kg_putih || 0), + kgRetak: formatNumber(item.kg_retak || 0), + kgPecah: formatNumber(item.kg_pecah || 0), + kgJumlah: formatNumber(item.kg_jumlah || 0), + totalKg: formatNumber(item.total_kg || 0), + persenUtuh: formatNumber(item.persen_utuh || 0), + persenPutih: formatNumber(item.persen_putih || 0), + persenRetak: formatNumber(item.persen_retak || 0), + persenPecah: formatNumber(item.persen_pecah || 0), + hd: formatNumber(item.hd || 0), + hdStd: formatNumber(item.hd_std || 0), + fi: formatNumber(item.fi || 0), + fiStd: formatNumber(item.fi_std || 0), + em: formatNumber(item.em || 0), + emStd: formatNumber(item.em_std || 0), + ew: formatNumber(item.ew || 0), + ewStd: formatNumber(item.ew_std || 0), + fcr: formatNumber(item.fcr || 0), + fcrStd: formatNumber(item.fcr_std || 0), + hh: formatNumber(item.hh || 0), + hhStd: formatNumber(item.hh_std || 0), + }); + }); + + const currentDate = new Date().toISOString().split('T')[0]; + const filename = params.period + ? `laporan-hasil-produksi-${params.period}.xlsx` + : `laporan-hasil-produksi-${currentDate}.xlsx`; + + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); +}; From 5d92e6774ec4d9a9396fe89f1193b926dc7aec94 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:14:00 +0700 Subject: [PATCH 081/149] feat(FE): Add stock field to StockLog type --- src/types/api/inventory/product.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/api/inventory/product.d.ts b/src/types/api/inventory/product.d.ts index f75e4060..134b982a 100644 --- a/src/types/api/inventory/product.d.ts +++ b/src/types/api/inventory/product.d.ts @@ -38,6 +38,7 @@ export type StockLog = { id: number; increase: number; decrease: number; + stock: number; loggable_type: string; loggable_id: number; notes: string; From 45ac8348fe84d949d63c4d0976a0a7a5d2cac7d1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:23:17 +0700 Subject: [PATCH 082/149] feat(FE): Add "Stock Akhir" column to StockLogTable --- .../pages/inventory/product/detail/StockLogTable.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/pages/inventory/product/detail/StockLogTable.tsx b/src/components/pages/inventory/product/detail/StockLogTable.tsx index 96d3dda6..0a305659 100644 --- a/src/components/pages/inventory/product/detail/StockLogTable.tsx +++ b/src/components/pages/inventory/product/detail/StockLogTable.tsx @@ -35,6 +35,13 @@ const StockLogTable = ({ header: 'Gudang', accessorKey: 'warehouse_name', }, + { + header: 'Stock Akhir', + accessorKey: 'stock', + cell: (props) => { + return formatNumber(props.row.original.stock); + }, + }, { header: 'Peningkatan', accessorKey: 'increase', From cb171118ee2eab5d7d873994d4a9716758564337 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:32:11 +0700 Subject: [PATCH 083/149] refactor(FE): Restrict row selection to specific approval criteria --- .../project-flock/ProjectFlockTable.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 4085bc56..5521fdbf 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -384,7 +384,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { id: 'select', header: ({ table }) => { const allRows = table.getRowModel().rows; - const selectableRows = allRows; + const selectableRows = allRows.filter((row) => { + const projectFlock = row.original; + return ( + projectFlock.approval?.step_number === 1 && + projectFlock.approval?.action !== 'REJECTED' + ); + }); const allSelected = selectableRows.every((row) => row.getIsSelected()) && @@ -398,6 +404,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { selectableRows.forEach((row) => row.toggleSelected(shouldSelect)); }; + const hasNoSelectableRows = selectableRows.length === 0; + return (
void }) => { checked={allSelected} indeterminate={someSelected} onChange={toggleSelectableRows} + disabled={hasNoSelectableRows} />
); @@ -845,6 +854,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setSorting={setSorting} rowSelection={rowSelection} setRowSelection={setRowSelection} + enableRowSelection={(row) => { + const projectFlock = row.original; + return ( + projectFlock.approval?.step_number === 1 && + projectFlock.approval?.action !== 'REJECTED' + ); + }} withCheckbox className={{ containerClassName: cn('p-3', { From 6aae18df54524d3aee0933836dc818feb7926c7c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 15:49:26 +0700 Subject: [PATCH 084/149] refactor(FE): Update import paths for finance and marketing tab stores --- .../pages/production/project-flock/ProjectFlockTable.tsx | 2 +- .../pages/production/project-flock/form/ProjectFlockForm.tsx | 2 +- src/components/pages/production/uniformity/UniformityTable.tsx | 2 +- .../pages/production/uniformity/form/UniformityForm.tsx | 2 +- .../pages/production/uniformity/form/UniformityPreviewForm.tsx | 2 +- .../pages/production/uniformity/form/UniformityResultForm.tsx | 2 +- src/components/pages/report/finance/FinanceTabs.tsx | 2 +- src/components/pages/report/finance/tab/CustomerPaymentTab.tsx | 2 +- src/components/pages/report/finance/tab/DebtSupplierTab.tsx | 2 +- .../pages/report/logistic-stock/LogisticStockTabs.tsx | 2 +- .../pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx | 2 +- src/components/pages/report/marketing/MarketingTabs.tsx | 2 +- src/components/pages/report/marketing/tab/DailyMarketingTab.tsx | 2 +- src/components/pages/report/marketing/tab/HppPerKandangTab.tsx | 2 +- .../{ => production}/project-flock/project-flock.store.ts | 2 +- .../project-flock/slices/project-flock.slice.ts | 0 .../{ => production}/uniformity/slices/uniformity.slice.ts | 0 src/stores/{ => production}/uniformity/uniformity.store.ts | 2 +- src/stores/{ => report}/finance-tab/finance-tab.store.ts | 0 .../{ => report}/logistic-stock-tab/logistic-stock-tab.store.ts | 0 src/stores/{ => report}/marketing-tab/marketing-tab.store.ts | 0 21 files changed, 16 insertions(+), 16 deletions(-) rename src/stores/{ => production}/project-flock/project-flock.store.ts (79%) rename src/stores/{ => production}/project-flock/slices/project-flock.slice.ts (100%) rename src/stores/{ => production}/uniformity/slices/uniformity.slice.ts (100%) rename src/stores/{ => production}/uniformity/uniformity.store.ts (80%) rename src/stores/{ => report}/finance-tab/finance-tab.store.ts (100%) rename src/stores/{ => report}/logistic-stock-tab/logistic-stock-tab.store.ts (100%) rename src/stores/{ => report}/marketing-tab/marketing-tab.store.ts (100%) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 5521fdbf..e8280fa8 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -34,7 +34,7 @@ import StatusBadge from '@/components/helper/StatusBadge'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal'; -import { useProjectFlockStore } from '@/stores/project-flock/project-flock.store'; +import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; const RowOptionsMenu = ({ diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index d56550a6..57cde405 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -52,7 +52,7 @@ import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import StatusBadge from '@/components/helper/StatusBadge'; import { getUniqueFormikErrors } from '@/lib/formik-helper'; -import { useProjectFlockStore } from '@/stores/project-flock/project-flock.store'; +import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; interface ProjectFlockFormProps { formType?: 'add' | 'edit' | 'detail'; diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 39112b47..3473967e 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -26,7 +26,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import UniformityTableSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityTableSkeleton'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import FloatingActionsButton from '@/components/FloatingActionsButton'; import Modal from '@/components/Modal'; import SelectInput, { diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 8ab62d85..33b649c4 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -7,7 +7,7 @@ import { Icon } from '@iconify/react'; import { toast } from 'react-hot-toast'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; diff --git a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx index 3cc120fd..3ca24952 100644 --- a/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityPreviewForm.tsx @@ -7,7 +7,7 @@ import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; import { diff --git a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx index eaf51103..108cb4f8 100644 --- a/src/components/pages/production/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx @@ -7,7 +7,7 @@ import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import { useUniformityStore } from '@/stores/production/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; import { useRouter } from 'next/navigation'; diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index ffb0d3f1..84d610a5 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; -import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; const FinanceTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 3680a41c..03b41e10 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -36,7 +36,7 @@ import { } from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; -import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { OptionType } from '@/components/table/TableRowSizeSelector'; import { Color } from '@/types/theme'; diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 5e7781bf..bd71f02a 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -33,7 +33,7 @@ import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useFinanceTabStore } from '@/stores/finance-tab/finance-tab.store'; +import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; import StatusBadge from '@/components/helper/StatusBadge'; import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index f06e63dc..6996db55 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; -import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; +import { useLogisticStockTabStore } from '@/stores/report/logistic-stock-tab/logistic-stock-tab.store'; const LogisticStockTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 23fb067e..f9251f4a 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -32,7 +32,7 @@ import { } from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useLogisticStockTabStore } from '@/stores/logistic-stock-tab/logistic-stock-tab.store'; +import { useLogisticStockTabStore } from '@/stores/report/logistic-stock-tab/logistic-stock-tab.store'; import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; interface PurchasesPerSupplierTabProps { diff --git a/src/components/pages/report/marketing/MarketingTabs.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx index de449f9c..d07c7d1b 100644 --- a/src/components/pages/report/marketing/MarketingTabs.tsx +++ b/src/components/pages/report/marketing/MarketingTabs.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; -import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; const MarketingReportContent = () => { const [activeTabId, setActiveTabId] = useState('1'); diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index a687fdf5..9c03de7a 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -37,7 +37,7 @@ import { import SelectInput from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; import { useEffect as useEffectHook } from 'react'; import { httpClient } from '@/services/http/client'; diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 514edcb9..0650c19a 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -33,7 +33,7 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useMarketingTabStore } from '@/stores/marketing-tab/marketing-tab.store'; +import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; import { useEffect as useEffectHook } from 'react'; diff --git a/src/stores/project-flock/project-flock.store.ts b/src/stores/production/project-flock/project-flock.store.ts similarity index 79% rename from src/stores/project-flock/project-flock.store.ts rename to src/stores/production/project-flock/project-flock.store.ts index 61efcb97..97402132 100644 --- a/src/stores/project-flock/project-flock.store.ts +++ b/src/stores/production/project-flock/project-flock.store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import { createProjectFlockSlice } from '@/stores/project-flock/slices/project-flock.slice'; +import { createProjectFlockSlice } from '@/stores/production/project-flock/slices/project-flock.slice'; import { ProjectFlockSlice } from '@/types/stores'; export type ProjectFlockStore = ProjectFlockSlice; diff --git a/src/stores/project-flock/slices/project-flock.slice.ts b/src/stores/production/project-flock/slices/project-flock.slice.ts similarity index 100% rename from src/stores/project-flock/slices/project-flock.slice.ts rename to src/stores/production/project-flock/slices/project-flock.slice.ts diff --git a/src/stores/uniformity/slices/uniformity.slice.ts b/src/stores/production/uniformity/slices/uniformity.slice.ts similarity index 100% rename from src/stores/uniformity/slices/uniformity.slice.ts rename to src/stores/production/uniformity/slices/uniformity.slice.ts diff --git a/src/stores/uniformity/uniformity.store.ts b/src/stores/production/uniformity/uniformity.store.ts similarity index 80% rename from src/stores/uniformity/uniformity.store.ts rename to src/stores/production/uniformity/uniformity.store.ts index c8d759d6..740d10b6 100644 --- a/src/stores/uniformity/uniformity.store.ts +++ b/src/stores/production/uniformity/uniformity.store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import { createUniformitySlice } from '@/stores/uniformity/slices/uniformity.slice'; +import { createUniformitySlice } from '@/stores/production/uniformity/slices/uniformity.slice'; import { UniformitySlice } from '@/types/stores'; export type UniformityStore = UniformitySlice; diff --git a/src/stores/finance-tab/finance-tab.store.ts b/src/stores/report/finance-tab/finance-tab.store.ts similarity index 100% rename from src/stores/finance-tab/finance-tab.store.ts rename to src/stores/report/finance-tab/finance-tab.store.ts diff --git a/src/stores/logistic-stock-tab/logistic-stock-tab.store.ts b/src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts similarity index 100% rename from src/stores/logistic-stock-tab/logistic-stock-tab.store.ts rename to src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts diff --git a/src/stores/marketing-tab/marketing-tab.store.ts b/src/stores/report/marketing-tab/marketing-tab.store.ts similarity index 100% rename from src/stores/marketing-tab/marketing-tab.store.ts rename to src/stores/report/marketing-tab/marketing-tab.store.ts From dbb523c7106560d57f86658a70184f4d0608a719 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 16:16:40 +0700 Subject: [PATCH 085/149] refactor(FE): Refactor tab store to use a unified ReportTabStore --- .../pages/report/finance/FinanceTabs.tsx | 4 +- .../report/finance/tab/CustomerPaymentTab.tsx | 6 +-- .../report/finance/tab/DebtSupplierTab.tsx | 6 +-- .../logistic-stock/LogisticStockTabs.tsx | 4 +- .../tab/PurchasesPerSupplierTab.tsx | 10 ++-- .../pages/report/marketing/MarketingTabs.tsx | 4 +- .../marketing/tab/DailyMarketingTab.tsx | 8 ++- .../report/marketing/tab/HppPerKandangTab.tsx | 8 ++- .../report/finance-tab/finance-tab.store.ts | 51 ------------------- .../logistic-stock-tab.store.ts | 51 ------------------- .../marketing-tab/marketing-tab.store.ts | 51 ------------------- src/stores/report/report-tab.store.ts | 18 +++++++ src/stores/report/slices/report-tab.slice.ts | 37 ++++++++++++++ 13 files changed, 76 insertions(+), 182 deletions(-) delete mode 100644 src/stores/report/finance-tab/finance-tab.store.ts delete mode 100644 src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts delete mode 100644 src/stores/report/marketing-tab/marketing-tab.store.ts create mode 100644 src/stores/report/report-tab.store.ts create mode 100644 src/stores/report/slices/report-tab.slice.ts diff --git a/src/components/pages/report/finance/FinanceTabs.tsx b/src/components/pages/report/finance/FinanceTabs.tsx index 84d610a5..de924f62 100644 --- a/src/components/pages/report/finance/FinanceTabs.tsx +++ b/src/components/pages/report/finance/FinanceTabs.tsx @@ -4,11 +4,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import CustomerPaymentTab from '@/components/pages/report/finance/tab/CustomerPaymentTab'; import DebtSupplierTab from '@/components/pages/report/finance/tab/DebtSupplierTab'; -import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; const FinanceTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useFinanceTabStore((state) => state.tabActions); + const tabActions = useReportTabStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 03b41e10..1443fa0b 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -36,7 +36,7 @@ import { } from '@/components/pages/report/finance/filter/CustomerPaymentFilter'; import { generateCustomerPaymentExcel } from '@/components/pages/report/finance/export/CustomerPaymentExportXLSX'; import { generateCustomerPaymentPDF } from '@/components/pages/report/finance/export/CustomerPaymentExportPDF'; -import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import CustomerSupplierSkeleton from '@/components/pages/report/finance/skeleton/CustomerSupplierSkeleton'; import { OptionType } from '@/components/table/TableRowSizeSelector'; import { Color } from '@/types/theme'; @@ -375,8 +375,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { }, [customerPaymentExport, filterParams, customerOptions]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useFinanceTabStore((state) => state.setTabActions); - const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index bd71f02a..44677313 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -33,7 +33,7 @@ import { Color } from '@/types/theme'; import { Supplier } from '@/types/api/master-data/supplier'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useFinanceTabStore } from '@/stores/report/finance-tab/finance-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import StatusBadge from '@/components/helper/StatusBadge'; import DebtSupplierSkeleton from '@/components/pages/report/finance/skeleton/DebtSupplierSkeleton'; @@ -271,8 +271,8 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { }, [debtSupplierExport]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useFinanceTabStore((state) => state.setTabActions); - const clearTabActions = useFinanceTabStore((state) => state.clearTabActions); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx index 6996db55..1e3f4109 100644 --- a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -3,11 +3,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; -import { useLogisticStockTabStore } from '@/stores/report/logistic-stock-tab/logistic-stock-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; const LogisticStockTabs = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useLogisticStockTabStore((state) => state.tabActions); + const tabActions = useReportTabStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index f9251f4a..5e2c4e27 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -32,7 +32,7 @@ import { } from '@/components/pages/report/logistic-stock/filter/PurchasesPerSupplierFilter'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; -import { useLogisticStockTabStore } from '@/stores/report/logistic-stock-tab/logistic-stock-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import PurchasePerSupplierSkeleton from '@/components/pages/report/logistic-stock/skeleton/PurchasePerSupplierSkeleton'; interface PurchasesPerSupplierTabProps { @@ -486,12 +486,8 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useLogisticStockTabStore( - (state) => state.setTabActions - ); - const clearTabActions = useLogisticStockTabStore( - (state) => state.clearTabActions - ); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffect(() => { setTabActions( diff --git a/src/components/pages/report/marketing/MarketingTabs.tsx b/src/components/pages/report/marketing/MarketingTabs.tsx index d07c7d1b..8a02a0c2 100644 --- a/src/components/pages/report/marketing/MarketingTabs.tsx +++ b/src/components/pages/report/marketing/MarketingTabs.tsx @@ -4,11 +4,11 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; import DailyMarketingReportContent from '@/components/pages/report/marketing/tab/DailyMarketingTab'; import HppPerKandangTab from '@/components/pages/report/marketing/tab/HppPerKandangTab'; -import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; const MarketingReportContent = () => { const [activeTabId, setActiveTabId] = useState('1'); - const tabActions = useMarketingTabStore((state) => state.tabActions); + const tabActions = useReportTabStore((state) => state.tabActions); const tabs = [ { diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 9c03de7a..df06c83c 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -37,7 +37,7 @@ import { import SelectInput from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; import { useEffect as useEffectHook } from 'react'; import { httpClient } from '@/services/http/client'; @@ -390,10 +390,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }, [dailyMarketingsExport, summaryTotal]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useMarketingTabStore((state) => state.setTabActions); - const clearTabActions = useMarketingTabStore( - (state) => state.clearTabActions - ); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffectHook(() => { setTabActions( diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index 0650c19a..d3d4bfac 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -33,7 +33,7 @@ import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import Modal, { useModal } from '@/components/Modal'; import { cn } from '@/lib/helper'; -import { useMarketingTabStore } from '@/stores/report/marketing-tab/marketing-tab.store'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; import HppPerKandangSkeleton from '@/components/pages/report/marketing/skeleton/HppPerKandangSkeleton'; import { useEffect as useEffectHook } from 'react'; @@ -487,10 +487,8 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { ]); // ===== REGISTER TAB ACTIONS TO STORE ===== - const setTabActions = useMarketingTabStore((state) => state.setTabActions); - const clearTabActions = useMarketingTabStore( - (state) => state.clearTabActions - ); + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); useEffectHook(() => { setTabActions( diff --git a/src/stores/report/finance-tab/finance-tab.store.ts b/src/stores/report/finance-tab/finance-tab.store.ts deleted file mode 100644 index 9b5cf096..00000000 --- a/src/stores/report/finance-tab/finance-tab.store.ts +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; - -export type FinanceTabActionsSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const useFinanceTabStore = create()( - devtools( - (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set( - (state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - }), - false, - 'setTabActions' - ), - - clearTabActions: (tabId) => - set( - (state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }, - false, - 'clearTabActions' - ), - - clearAllTabActions: () => - set({ tabActions: {} }, false, 'clearAllTabActions'), - }), - { - name: 'FinanceTabStore', - } - ) -); diff --git a/src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts b/src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts deleted file mode 100644 index f9e142b1..00000000 --- a/src/stores/report/logistic-stock-tab/logistic-stock-tab.store.ts +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; - -export type LogisticStockTabActionsSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const useLogisticStockTabStore = create()( - devtools( - (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set( - (state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - }), - false, - 'setTabActions' - ), - - clearTabActions: (tabId) => - set( - (state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }, - false, - 'clearTabActions' - ), - - clearAllTabActions: () => - set({ tabActions: {} }, false, 'clearAllTabActions'), - }), - { - name: 'LogisticStockTabStore', - } - ) -); diff --git a/src/stores/report/marketing-tab/marketing-tab.store.ts b/src/stores/report/marketing-tab/marketing-tab.store.ts deleted file mode 100644 index 153bbb8d..00000000 --- a/src/stores/report/marketing-tab/marketing-tab.store.ts +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { ReactNode } from 'react'; -import { create } from 'zustand'; -import { devtools } from 'zustand/middleware'; - -export type MarketingTabActionsSlice = { - // State - actions per tab ID - tabActions: Record; - - // Actions - setTabActions: (tabId: string, actions: ReactNode) => void; - clearTabActions: (tabId: string) => void; - clearAllTabActions: () => void; -}; - -export const useMarketingTabStore = create()( - devtools( - (set) => ({ - tabActions: {}, - - setTabActions: (tabId, actions) => - set( - (state) => ({ - tabActions: { - ...state.tabActions, - [tabId]: actions, - }, - }), - false, - 'setTabActions' - ), - - clearTabActions: (tabId) => - set( - (state) => { - const { [tabId]: _, ...rest } = state.tabActions; - return { tabActions: rest }; - }, - false, - 'clearTabActions' - ), - - clearAllTabActions: () => - set({ tabActions: {} }, false, 'clearAllTabActions'), - }), - { - name: 'MarketingTabStore', - } - ) -); diff --git a/src/stores/report/report-tab.store.ts b/src/stores/report/report-tab.store.ts new file mode 100644 index 00000000..76a2c684 --- /dev/null +++ b/src/stores/report/report-tab.store.ts @@ -0,0 +1,18 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createReportTabSlice, ReportTabSlice } from '@/stores/report/slices/report-tab.slice'; + +export type ReportTabStore = ReportTabSlice; + +export const useReportTabStore = create()( + devtools( + (...args) => ({ + ...createReportTabSlice(...args), + }), + { + name: 'ReportTabStore', + } + ) +); diff --git a/src/stores/report/slices/report-tab.slice.ts b/src/stores/report/slices/report-tab.slice.ts new file mode 100644 index 00000000..6582eaed --- /dev/null +++ b/src/stores/report/slices/report-tab.slice.ts @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { StateCreator } from 'zustand'; + +export type ReportTabSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const createReportTabSlice: StateCreator< + ReportTabSlice, + [], + [], + ReportTabSlice +> = (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set((state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + })), + + clearTabActions: (tabId) => + set((state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }), + + clearAllTabActions: () => set({ tabActions: {} }), +}); From 211622c7b06fa5d79c3a2d874ecba619c45f5d84 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 12 Feb 2026 16:19:40 +0700 Subject: [PATCH 086/149] refactor(FE): Format import statements in report-tab.store.ts --- src/stores/report/report-tab.store.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/stores/report/report-tab.store.ts b/src/stores/report/report-tab.store.ts index 76a2c684..aad47d17 100644 --- a/src/stores/report/report-tab.store.ts +++ b/src/stores/report/report-tab.store.ts @@ -2,7 +2,10 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import { createReportTabSlice, ReportTabSlice } from '@/stores/report/slices/report-tab.slice'; +import { + createReportTabSlice, + ReportTabSlice, +} from '@/stores/report/slices/report-tab.slice'; export type ReportTabStore = ReportTabSlice; From 5c00893ea3fbf4db448fe34b8d2812e1a0f471e3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:24:42 +0700 Subject: [PATCH 087/149] refactor(FE): Refactor production result components and improve UI --- src/app/report/production-result/page.tsx | 4 +- ...oductionResultProjectFlockKandangTable.tsx | 137 +-- .../ProductionResultTabs.tsx | 39 + .../filter/ProductionResultFilter.ts | 41 +- .../skeleton/ProductionResultSkeleton.tsx | 15 +- .../tab/ProductionResultTab.tsx | 919 +++++++++++------- 6 files changed, 682 insertions(+), 473 deletions(-) create mode 100644 src/components/pages/report/production-result/ProductionResultTabs.tsx diff --git a/src/app/report/production-result/page.tsx b/src/app/report/production-result/page.tsx index fb8f2a0c..4c9ea02b 100644 --- a/src/app/report/production-result/page.tsx +++ b/src/app/report/production-result/page.tsx @@ -1,9 +1,9 @@ -import ProductionResultContent from '@/components/pages/report/production-result/tab/ProductionResultTab'; +import ProductionResultTabs from '@/components/pages/report/production-result/ProductionResultTabs'; const ProductionResultReportPage = () => { return (
- +
); }; diff --git a/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx b/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx index e1dfd515..ded97d02 100644 --- a/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx +++ b/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx @@ -4,12 +4,10 @@ import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatNumber } from '@/lib/helper'; +import { formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { ProductionResult } from '@/types/api/report/production-result'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -52,8 +50,6 @@ const ProductionResultProjectFlockKandangTable = ({ } ); - const [open, setOpen] = useState(false); - const [sorting, setSorting] = useState([]); const productionResultColumns: ColumnDef[] = [ @@ -270,93 +266,60 @@ const ProductionResultProjectFlockKandangTable = ({ } }, [sorting]); - useEffect(() => { - if (!open) { - setOpen( + return ( + 0 : false - ); - } - }, [productionResults, isResponseSuccess]); - - return ( - - -
{kandangName}
- - - + + data={ + isResponseSuccess(productionResults) ? productionResults?.data : [] } - className='w-full!' - titleClassName='w-full p-0!' - > -
- {/*
-
- -
-
*/} - - - data={ - isResponseSuccess(productionResults) - ? productionResults?.data - : [] - } - columns={productionResultColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(productionResults) - ? productionResults?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(productionResults) - ? productionResults?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingProductionResults} - sorting={sorting} - setSorting={setSorting} - renderFooter={false} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(productionResults) && - productionResults?.data?.length === 0, - }), - headerColumnClassName: - 'px-4 py-3 border-x border-base-content/10 text-base-content/50', - }} - /> -
-
+ columns={productionResultColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(productionResults) + ? productionResults?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(productionResults) + ? productionResults?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingProductionResults} + sorting={sorting} + setSorting={setSorting} + renderFooter={false} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + />
); }; diff --git a/src/components/pages/report/production-result/ProductionResultTabs.tsx b/src/components/pages/report/production-result/ProductionResultTabs.tsx new file mode 100644 index 00000000..2bd7765f --- /dev/null +++ b/src/components/pages/report/production-result/ProductionResultTabs.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { useState } from 'react'; +import Tabs from '@/components/Tabs'; +import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultTab'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; + +const ProductionResultTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useReportTabStore((state) => state.tabActions); + + const tabs = [ + { + id: '1', + label: 'Hasil Produksi', + content: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default ProductionResultTabs; diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts index 1c1979eb..3281bb19 100644 --- a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts +++ b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts @@ -1,48 +1,17 @@ import * as yup from 'yup'; -export type ProductionResultFilterType = { +export type ProductionResultFilterProps = { area_id: string | null; location_id: string | null; project_flock_id: string | null; kandang_id: string | null; - date_start: string | null; - date_end: string | null; - sort_by: string | null; - show_unrecorded: boolean | null; }; export const ProductionResultFilterSchema = yup.object({ - area_id: yup.string().nullable(), - location_id: yup.string().nullable(), - project_flock_id: yup.string().nullable(), - kandang_id: yup.string().nullable(), - date_start: yup - .string() - .nullable() - .test( - 'is-valid-date', - 'Tanggal mulai tidak valid', - (value) => !value || !isNaN(Date.parse(value)) - ), - date_end: yup - .string() - .nullable() - .test( - 'is-valid-date', - 'Tanggal akhir tidak valid', - (value) => !value || !isNaN(Date.parse(value)) - ) - .test( - 'is-after-start', - 'Tanggal akhir tidak boleh lebih awal dari tanggal mulai', - function (value) { - const { date_start } = this.parent; - if (!date_start || !value) return true; - return new Date(value) >= new Date(date_start); - } - ), - sort_by: yup.string().nullable(), - show_unrecorded: yup.boolean().nullable(), + area_id: yup.string().required('Area wajib dipilih'), + location_id: yup.string().required('Lokasi wajib dipilih'), + project_flock_id: yup.string().required('Project Flock wajib dipilih'), + kandang_id: yup.string().required('Kandang wajib dipilih'), }); export type ProductionResultFilterValues = yup.InferType< diff --git a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx index c8cd63d8..07d33233 100644 --- a/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx +++ b/src/components/pages/report/production-result/skeleton/ProductionResultSkeleton.tsx @@ -4,13 +4,26 @@ import Table from '@/components/Table'; import { ProductionResult } from '@/types/api/report/production-result'; import { ColumnDef } from '@tanstack/react-table'; +type ProductionResultColumn = + | ColumnDef + | { + header: string; + columns: Array<{ + header: string; + accessorKey?: string; + cell?: (props: { + row: { original: ProductionResult }; + }) => React.ReactNode; + }>; + }; + const ProductionResultSkeleton = ({ columns, icon, title, subtitle, }: { - columns: ColumnDef[]; + columns: ProductionResultColumn[]; icon: React.ReactNode; title: string; subtitle: string; diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx index ac5e76fd..2d7915e8 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -1,6 +1,7 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import useSWR from 'swr'; import { generateProductionResultExcel } from '../export/ProductionResultExportXLSX'; import toast from 'react-hot-toast'; @@ -13,8 +14,9 @@ import SelectInput, { } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; -import Card from '@/components/Card'; import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable'; +import { useFormik } from 'formik'; +import { ProductionResultFilterSchema } from '@/components/pages/report/production-result/filter/ProductionResultFilter'; import { BaseKandang } from '@/types/api/master-data/kandang'; import { AreaApi, LocationApi } from '@/services/api/master-data'; @@ -26,87 +28,248 @@ import { BaseProjectFlockKandang, ProjectFlockKandang, } from '@/types/api/production/project-flock-kandang'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import Pagination from '@/components/Pagination'; +import { isResponseSuccess } from '@/lib/api-helper'; import { ProductionResultReportApi } from '@/services/api/report/production-result'; import { BaseApiResponse } from '@/types/api/api-general'; import { httpClient } from '@/services/http/client'; +import { ColumnDef } from '@tanstack/react-table'; import { ProductionResult } from '@/types/api/report/production-result'; import ProductionResultReportPDF from '../export/ProductionResultExportPDF'; import { pdf } from '@react-pdf/renderer'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import Modal, { useModal } from '@/components/Modal'; +import { cn, formatNumber } from '@/lib/helper'; +import Pagination from '@/components/Pagination'; +import ProductionResultSkeleton from '@/components/pages/report/production-result/skeleton/ProductionResultSkeleton'; -const ProductionResultContent = () => { - const [projectFlockKandangs, setProjectFlockKandangs] = useState< - ProjectFlockKandang[] | null - >(null); - const [projectFlockKandangMetadata, setProjectFlockKandangMetadata] = - useState< - | { - page: number; - limit: number; - total_pages: number; - total_results: number; - } - | undefined - >(undefined); +interface ProductionResultTabProps { + tabId: string; +} +interface FilterParams { + area_id?: string; + location_id?: string; + project_flock_id?: string; + project_flock_kandang_id?: string; +} + +type ProductionResultFilterFormValues = { + area_id: OptionType | null; + location_id: OptionType | null; + project_flock_id: OptionType | null; + kandang_id: OptionType | null; +}; + +const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + const [filterParams, setFilterParams] = useState({}); + + // ===== PAGINATION STATE ===== const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); - const [isLoadingSearch, setIsLoadingSearch] = useState(false); + const filterModal = useModal(); - const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = - useState(false); + // ===== TABLE COLUMNS ===== + const productionResultColumns: ColumnDef[] = [ + { + header: 'No', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'woa', + header: 'WOA', + }, + { + accessorKey: 'bw', + header: 'BW', + cell: (props) => formatNumber(props.row.original.bw), + }, + { + accessorKey: 'std_bw', + header: 'STD BW', + cell: (props) => formatNumber(props.row.original.std_bw), + }, + { + accessorKey: 'uniformity', + header: 'Uniformity', + cell: (props) => formatNumber(props.row.original.uniformity), + }, + { + accessorKey: 'std_uniformity', + header: 'STD Uniformity', + }, + { + accessorKey: 'dep_kum', + header: 'Dep Kum', + cell: (props) => formatNumber(props.row.original.dep_kum), + }, + { + accessorKey: 'dep_std', + header: 'Dep STD', + cell: (props) => formatNumber(props.row.original.dep_std), + }, + { + header: 'Butiran', + columns: [ + { + accessorKey: 'butiran_utuh', + header: 'Utuh', + cell: (props) => formatNumber(props.row.original.butiran_utuh), + }, + { + accessorKey: 'butiran_putih', + header: 'Putih', + cell: (props) => formatNumber(props.row.original.butiran_putih), + }, + { + accessorKey: 'butiran_retak', + header: 'Retak', + cell: (props) => formatNumber(props.row.original.butiran_retak), + }, + { + accessorKey: 'butiran_pecah', + header: 'Pecah', + cell: (props) => formatNumber(props.row.original.butiran_pecah), + }, + { + accessorKey: 'butiran_jumlah', + header: 'Jumlah (Butir)', + cell: (props) => formatNumber(props.row.original.butiran_jumlah), + }, + { + accessorKey: 'total_butir', + header: 'Total Butir', + cell: (props) => formatNumber(props.row.original.total_butir), + }, + ], + }, + { + header: 'Kg', + columns: [ + { + accessorKey: 'kg_utuh', + header: 'Utuh (Kg)', + cell: (props) => formatNumber(props.row.original.kg_utuh), + }, + { + accessorKey: 'kg_putih', + header: 'Putih (Kg)', + cell: (props) => formatNumber(props.row.original.kg_putih), + }, + { + accessorKey: 'kg_retak', + header: 'Retak (Kg)', + cell: (props) => formatNumber(props.row.original.kg_retak), + }, + { + accessorKey: 'kg_pecah', + header: 'Pecah (Kg)', + cell: (props) => formatNumber(props.row.original.kg_pecah), + }, + { + accessorKey: 'kg_jumlah', + header: 'Jumlah (Kg)', + cell: (props) => formatNumber(props.row.original.kg_jumlah), + }, + { + accessorKey: 'total_kg', + header: 'Total Kg', + cell: (props) => formatNumber(props.row.original.total_kg), + }, + ], + }, + { + header: '%', + columns: [ + { + accessorKey: 'persen_utuh', + header: 'Utuh', + cell: (props) => formatNumber(props.row.original.persen_utuh), + }, + { + accessorKey: 'persen_putih', + header: 'Putih', + cell: (props) => formatNumber(props.row.original.persen_putih), + }, + { + accessorKey: 'persen_retak', + header: 'Retak', + cell: (props) => formatNumber(props.row.original.persen_retak), + }, + { + accessorKey: 'persen_pecah', + header: 'Pecah', + cell: (props) => formatNumber(props.row.original.persen_pecah), + }, + ], + }, + ]; - const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false); - - const [selectedArea, setSelectedArea] = useState(null); - const [selectedLocation, setSelectedLocation] = useState( - null - ); - const [selectedProjectFlock, setSelectedProjectFlock] = - useState(null); - const [selectedProjectFlockKandang, setSelectedProjectFlockKandang] = - useState(null); + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + area_id: null, + location_id: null, + project_flock_id: null, + kandang_id: null, + }, + validationSchema: ProductionResultFilterSchema, + onSubmit: (values) => { + setFilterParams({ + area_id: values.area_id?.value + ? String(values.area_id.value) + : undefined, + location_id: values.location_id?.value + ? String(values.location_id.value) + : undefined, + project_flock_id: values.project_flock_id?.value + ? String(values.project_flock_id.value) + : undefined, + project_flock_kandang_id: values.kandang_id?.value + ? String(values.kandang_id.value) + : undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setPage(1); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setPage(1); + }, + }); + // ===== OPTIONS ===== const { setInputValue: setAreaInputValue, options: areaOptions, - isLoadingOptions: isLoadingAreaOptions, + isLoadingOptions: isLoadingAreas, loadMore: loadMoreAreas, - } = useSelect(AreaApi.basePath, 'id', 'name'); - - const areaChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedArea(val as OptionType); - - setSelectedLocation(null); - - setSelectedProjectFlock(null); - - setSelectedProjectFlockKandang(null); - }; + } = useSelect(AreaApi.basePath, 'id', 'name', 'search'); const { setInputValue: setLocationInputValue, options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, + isLoadingOptions: isLoadingLocations, loadMore: loadMoreLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { - area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '', + area_id: formik.values.area_id?.value + ? String(formik.values.area_id.value) + : '', }); - const locationChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedLocation(val as OptionType); - - setSelectedProjectFlock(null); - - setSelectedProjectFlockKandang(null); - }; - const { setInputValue: setProjectFlockInputValue, options: projectFlockOptions, - isLoadingOptions: isLoadingProjectFlockOptions, + isLoadingOptions: isLoadingProjectFlocks, loadMore: loadMoreProjectFlocks, } = useSelect( ProjectFlockApi.basePath, @@ -114,26 +277,20 @@ const ProductionResultContent = () => { 'flock_name', 'search', { - area_id: selectedArea - ? ((selectedArea as OptionType).value as string) + area_id: formik.values.area_id?.value + ? String(formik.values.area_id.value) : '', - location_id: selectedLocation - ? ((selectedLocation as OptionType).value as string) + location_id: formik.values.location_id?.value + ? String(formik.values.location_id.value) : '', category: 'LAYING', } ); - const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { - setSelectedProjectFlock(val as OptionType); - - setSelectedProjectFlockKandang(null); - }; - const { setInputValue: setProjectFlockKandangInputValue, options: projectFlockKandangOptions, - isLoadingOptions: isLoadingProjectFlockKandangOptions, + isLoadingOptions: isLoadingProjectFlockKandangs, loadMore: loadMoreProjectFlockKandangs, } = useSelect( ProjectFlockKandangApi.basePath, @@ -141,37 +298,103 @@ const ProductionResultContent = () => { 'kandang.name', 'search', { - area_id: selectedArea - ? ((selectedArea as OptionType).value as string) + area_id: formik.values.area_id?.value + ? String(formik.values.area_id.value) : '', - location_id: selectedLocation - ? ((selectedLocation as OptionType).value as string) + location_id: formik.values.location_id?.value + ? String(formik.values.location_id.value) : '', - project_flock_id: selectedProjectFlock - ? ((selectedProjectFlock as OptionType).value as string) + project_flock_id: formik.values.project_flock_id?.value + ? String(formik.values.project_flock_id.value) : '', } ); - const projectFlockKandangChangeHandler = ( - val: OptionType | OptionType[] | null - ) => { - setSelectedProjectFlockKandang(val as OptionType); - }; + // ===== FILTER HELPERS ===== + const areaValue = useMemo( + () => formik.values.area_id, + [formik.values.area_id] + ); - const exportToExcelHandler = async () => { - setIsLoadingExportingToExcel(true); + const locationValue = useMemo( + () => formik.values.location_id, + [formik.values.location_id] + ); + + const projectFlockValue = useMemo( + () => formik.values.project_flock_id, + [formik.values.project_flock_id] + ); + + const projectFlockKandangValue = useMemo( + () => formik.values.kandang_id, + [formik.values.kandang_id] + ); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (filterParams.area_id) count += 1; + if (filterParams.location_id) count += 1; + if (filterParams.project_flock_id) count += 1; + if (filterParams.project_flock_kandang_id) count += 1; + + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: projectFlockKandangsData, isLoading } = useSWR< + BaseApiResponse + >( + isSubmitted + ? () => { + const params = new URLSearchParams(); + if (filterParams.area_id) + params.append('area_id', filterParams.area_id); + if (filterParams.project_flock_id) + params.append('project_flock_id', filterParams.project_flock_id); + params.append('page', String(page)); + params.append('limit', String(pageSize)); + + return [`/production/project-flock-kandangs?${params.toString()}`]; + } + : null, + ([url]: string[]) => httpClient>(url) + ); + + const projectFlockKandangs = useMemo( + () => + isResponseSuccess(projectFlockKandangsData) + ? projectFlockKandangsData.data + : null, + [projectFlockKandangsData] + ); + + const projectFlockKandangMetadata = useMemo( + () => + isResponseSuccess(projectFlockKandangsData) + ? projectFlockKandangsData.meta + : undefined, + [projectFlockKandangsData] + ); + + // ===== EXPORT HANDLERS ===== + const exportToExcelHandler = useCallback(async () => { + setIsExcelExportLoading(true); try { - let projectFlockKandangsData: BaseProjectFlockKandang[] = []; + let projectFlockKandangsFetch: BaseProjectFlockKandang[] = []; - if (selectedProjectFlockKandang) { + if (filterParams.project_flock_kandang_id) { const projectFlockKandangResponse = await ProjectFlockKandangApi.getSingle( - selectedProjectFlockKandang?.value as number + Number(filterParams.project_flock_kandang_id) ); - projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsFetch = isResponseSuccess( projectFlockKandangResponse ) ? [projectFlockKandangResponse.data] @@ -179,11 +402,11 @@ const ProductionResultContent = () => { } else { const projectFlockKandangsResponse = await ProjectFlockKandangApi.getAll({ - area_id: selectedArea?.value, - project_flock_id: selectedProjectFlock?.value, + area_id: filterParams.area_id, + project_flock_id: filterParams.project_flock_id, }); - projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsFetch = isResponseSuccess( projectFlockKandangsResponse ) ? projectFlockKandangsResponse.data @@ -192,7 +415,7 @@ const ProductionResultContent = () => { const productionResultData: ProductionResult[] = []; - for (const kandang of projectFlockKandangsData) { + for (const kandang of projectFlockKandangsFetch) { const getProductionResultPath = `${ProductionResultReportApi.basePath}/${kandang.id}?page=1&limit=100`; const getProductionResultRes = await httpClient< BaseApiResponse @@ -205,7 +428,7 @@ const ProductionResultContent = () => { project_flock: { ...result.project_flock, name: - selectedProjectFlock?.label || + projectFlockValue?.label || kandang.project_flock?.name || `Project Flock #${kandang.project_flock_id}`, category: kandang.project_flock?.category || '', @@ -222,7 +445,7 @@ const ProductionResultContent = () => { if (productionResultData.length === 0) { toast.error('Tidak ada data untuk diexport.'); - setIsLoadingExportingToExcel(false); + setIsExcelExportLoading(false); return; } @@ -233,23 +456,23 @@ const ProductionResultContent = () => { } catch { toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); } finally { - setIsLoadingExportingToExcel(false); + setIsExcelExportLoading(false); } - }; + }, [filterParams, projectFlockValue]); - const exportToPdfHandler = async () => { - setIsLoadingExportingToPdf(true); + const exportToPdfHandler = useCallback(async () => { + setIsPdfExportLoading(true); try { - let projectFlockKandangsData: BaseProjectFlockKandang[] = []; + let projectFlockKandangsFetch: BaseProjectFlockKandang[] = []; - if (selectedProjectFlockKandang) { + if (filterParams.project_flock_kandang_id) { const projectFlockKandangResponse = await ProjectFlockKandangApi.getSingle( - selectedProjectFlockKandang?.value as number + Number(filterParams.project_flock_kandang_id) ); - projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsFetch = isResponseSuccess( projectFlockKandangResponse ) ? [projectFlockKandangResponse.data] @@ -257,11 +480,11 @@ const ProductionResultContent = () => { } else { const projectFlockKandangsResponse = await ProjectFlockKandangApi.getAll({ - area_id: selectedArea?.value, - project_flock_id: selectedProjectFlock?.value, + area_id: filterParams.area_id, + project_flock_id: filterParams.project_flock_id, }); - projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsFetch = isResponseSuccess( projectFlockKandangsResponse ) ? projectFlockKandangsResponse.data @@ -272,7 +495,7 @@ const ProductionResultContent = () => { projectFlockKandang: BaseProjectFlockKandang; productionResult: ProductionResult[] | null; }[] = await Promise.all( - projectFlockKandangsData.map(async (projectFlockKandang) => { + projectFlockKandangsFetch.map(async (projectFlockKandang) => { const getProductionResultPath = `${ProductionResultReportApi.basePath}/${projectFlockKandang.id}?page=1&limit=100`; const getProductionResultRes = await httpClient< BaseApiResponse @@ -289,7 +512,7 @@ const ProductionResultContent = () => { if (mappedProductionResults.length === 0) { toast.error('Tidak ada data untuk diexport.'); - setIsLoadingExportingToPdf(false); + setIsPdfExportLoading(false); return; } @@ -312,258 +535,141 @@ const ProductionResultContent = () => { toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); } - setIsLoadingExportingToPdf(false); - }; + setIsPdfExportLoading(false); + }, [filterParams]); - const searchHandler = async () => { - setProjectFlockKandangs(null); - setIsLoadingSearch(true); + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); - try { - if (selectedProjectFlockKandang) { - const projectFlockKandangResponse = - await ProjectFlockKandangApi.getSingle( - selectedProjectFlockKandang?.value as number - ); + useEffect(() => { + setTabActions( + tabId, +
+ - if ( - !projectFlockKandangResponse || - isResponseError(projectFlockKandangResponse) - ) { - throw new Error(); - } + + + Export +
+ +
+ + } + align='end' + className={{ + content: + 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', + }} + > + + + + +
+
+ ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + exportToExcelHandler, + exportToPdfHandler, + setTabActions, + ]); - setProjectFlockKandangs([projectFlockKandangResponse.data]); - setProjectFlockKandangMetadata({ - page: 1, - limit: 10, - total_pages: 1, - total_results: 1, - }); - setIsLoadingSearch(false); - return; - } - - const projectFlockKandangsResponse = await ProjectFlockKandangApi.getAll({ - area_id: selectedArea?.value, - project_flock_id: selectedProjectFlock?.value, - }); - - if ( - !projectFlockKandangsResponse || - isResponseError(projectFlockKandangsResponse) - ) { - throw new Error(); - } - - setProjectFlockKandangs(projectFlockKandangsResponse.data); - setProjectFlockKandangMetadata(projectFlockKandangsResponse.meta); - setIsLoadingSearch(false); - } catch (error) { - toast.error('Gagal mencari data! Coba lagi.'); - setProjectFlockKandangs(null); - setProjectFlockKandangMetadata(undefined); - setIsLoadingSearch(false); - } - }; - - const resetHandler = () => { - setProjectFlockKandangs(null); - setSelectedArea(null); - setSelectedLocation(null); - setSelectedProjectFlock(null); - setSelectedProjectFlockKandang(null); - // resetFilter(); - }; + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); return ( -
- -
-

- Laporan Hasil Produksi -

-
- - {/* Filters */} -
-
- - - - - - - -
- -
-
- - - - - Export{' '} - - - } - > - - - - - -
-
-
-
- -
- {isLoadingSearch && ( - - )} - - {!isLoadingSearch && !projectFlockKandangs && ( -

- Silakan pilih filter dan klik tombol Cari untuk menampilkan data. -

- )} - - {!isLoadingSearch && projectFlockKandangs?.length === 0 && ( -

- Tidak ada data kandang project flock yang dapat ditampilkan. -

- )} - - {!isLoadingSearch && projectFlockKandangs && ( - - {projectFlockKandangs.map((projectFlockKandang) => ( - +
+ {!isSubmitted ? ( + - ))} + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( +
+ +
+ ) : !projectFlockKandangs || projectFlockKandangs.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( + <> + {projectFlockKandangs.map( + (projectFlockKandang: ProjectFlockKandang) => ( + + ) + )} -
+
{ onRowChange={setPageSize} />
- + )}
-
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+ +
+ {/* Modal Body */} +
+ { + formik.setFieldValue('area_id', val); + formik.setFieldValue('location_id', null); + formik.setFieldValue('project_flock_id', null); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setAreaInputValue} + onMenuScrollToBottom={loadMoreAreas} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('location_id', val); + formik.setFieldValue('project_flock_id', null); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} + isClearable + isDisabled={!formik.values.area_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('project_flock_id', val); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setProjectFlockInputValue} + onMenuScrollToBottom={loadMoreProjectFlocks} + isClearable + isDisabled={!formik.values.location_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('kandang_id', val); + }} + onInputChange={setProjectFlockKandangInputValue} + onMenuScrollToBottom={loadMoreProjectFlockKandangs} + isClearable + isDisabled={!formik.values.project_flock_id} + className={{ wrapper: 'w-full' }} + /> +
+ + {/* Modal Footer */} +
+ + +
+ +
+ ); }; From d5962f94a1ff0be63114e5d5721dd6fecdef826a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:29:37 +0700 Subject: [PATCH 088/149] refactor(FE): Refactor production result filter to use OptionType --- .../filter/ProductionResultFilter.ts | 50 +++++++++++++++++-- .../tab/ProductionResultTab.tsx | 44 +++++++++------- 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts index 3281bb19..6df3759e 100644 --- a/src/components/pages/report/production-result/filter/ProductionResultFilter.ts +++ b/src/components/pages/report/production-result/filter/ProductionResultFilter.ts @@ -1,3 +1,4 @@ +import { OptionType } from '@/components/input/SelectInput'; import * as yup from 'yup'; export type ProductionResultFilterProps = { @@ -7,12 +8,51 @@ export type ProductionResultFilterProps = { kandang_id: string | null; }; +export type ProductionResultFilterFormType = { + area_id: OptionType | null; + location_id: OptionType | null; + project_flock_id: OptionType | null; + kandang_id: OptionType | null; +}; + export const ProductionResultFilterSchema = yup.object({ - area_id: yup.string().required('Area wajib dipilih'), - location_id: yup.string().required('Lokasi wajib dipilih'), - project_flock_id: yup.string().required('Project Flock wajib dipilih'), - kandang_id: yup.string().required('Kandang wajib dipilih'), -}); + area_id: yup + .mixed() + .required('Area wajib dipilih') + .test('is-not-empty', 'Area wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + location_id: yup + .mixed() + .required('Lokasi wajib dipilih') + .test('is-not-empty', 'Lokasi wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + project_flock_id: yup + .mixed() + .required('Project Flock wajib dipilih') + .test('is-not-empty', 'Project Flock wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), + kandang_id: yup + .mixed() + .required('Kandang wajib dipilih') + .test('is-not-empty', 'Kandang wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return !!value; + }), +}) as yup.ObjectSchema; export type ProductionResultFilterValues = yup.InferType< typeof ProductionResultFilterSchema diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx index 2d7915e8..090ca781 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -8,15 +8,15 @@ import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Dropdown from '@/components/dropdown/Dropdown'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable'; import { useFormik } from 'formik'; -import { ProductionResultFilterSchema } from '@/components/pages/report/production-result/filter/ProductionResultFilter'; +import { + ProductionResultFilterSchema, + type ProductionResultFilterValues, +} from '@/components/pages/report/production-result/filter/ProductionResultFilter'; import { BaseKandang } from '@/types/api/master-data/kandang'; import { AreaApi, LocationApi } from '@/services/api/master-data'; @@ -53,13 +53,6 @@ interface FilterParams { project_flock_kandang_id?: string; } -type ProductionResultFilterFormValues = { - area_id: OptionType | null; - location_id: OptionType | null; - project_flock_id: OptionType | null; - kandang_id: OptionType | null; -}; - const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); @@ -213,7 +206,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { ]; // ===== FORMIK SETUP ===== - const formik = useFormik({ + const formik = useFormik({ initialValues: { area_id: null, location_id: null, @@ -221,6 +214,8 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { kandang_id: null, }, validationSchema: ProductionResultFilterSchema, + validateOnBlur: true, + validateOnChange: true, onSubmit: (values) => { setFilterParams({ area_id: values.area_id?.value @@ -723,7 +718,7 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { {/* Modal Body */}
{ onInputChange={setAreaInputValue} onMenuScrollToBottom={loadMoreAreas} isClearable + isError={formik.touched.area_id && Boolean(formik.errors.area_id)} + errorMessage={formik.errors.area_id} className={{ wrapper: 'w-full' }} /> { onMenuScrollToBottom={loadMoreLocations} isClearable isDisabled={!formik.values.area_id} + isError={ + formik.touched.location_id && Boolean(formik.errors.location_id) + } + errorMessage={formik.errors.location_id} className={{ wrapper: 'w-full' }} /> { onMenuScrollToBottom={loadMoreProjectFlocks} isClearable isDisabled={!formik.values.location_id} + isError={ + formik.touched.project_flock_id && + Boolean(formik.errors.project_flock_id) + } + errorMessage={formik.errors.project_flock_id} className={{ wrapper: 'w-full' }} /> { onMenuScrollToBottom={loadMoreProjectFlockKandangs} isClearable isDisabled={!formik.values.project_flock_id} + isError={ + formik.touched.kandang_id && Boolean(formik.errors.kandang_id) + } + errorMessage={formik.errors.kandang_id} className={{ wrapper: 'w-full' }} />
From 684f67593fcc60c948e7bc91015a0ea42d39d15a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:32:45 +0700 Subject: [PATCH 089/149] refactor(FE): Refactor SelectInput styles for improved readability --- src/components/input/SelectInput.tsx | 53 ++++++++++++---------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 38be09e4..ef959ea7 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -284,23 +284,22 @@ const SelectInput = (props: SelectInputProps) => { isDisabled && !readOnly, 'bg-transparent! cursor-not-allowed!': readOnly, 'cursor-pointer!': !readOnly && !isDisabled, - 'border-red-500! ring-2 ring-red-200': isError, + 'border-error!': isError, + 'ring-2 ring-error/20': isError, 'border-indigo-500 ring-2 ring-indigo-200': - isFocused && !startAdornment, + isFocused && !startAdornment && !isError, 'border-base-content/10!': !isError && !isFocused, 'rounded-l-none!': inputPrefix && !startAdornment, 'rounded-r-none!': inputSuffix && !startAdornment, }), valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), placeholder: () => - cn({ - 'text-gray-400 text-sm leading-tight': !isError, - 'text-red-300!': isError, + cn('text-gray-400 text-sm leading-tight', { + 'text-error!': isError, }), singleValue: () => - cn({ - 'm-0! text-gray-900 text-sm leading-tight': !isError, - 'text-error!': isError, + cn('m-0! text-gray-900 text-sm leading-tight', { + 'text-error!': isError && !readOnly, 'text-gray-900!': readOnly, }), input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), @@ -404,32 +403,26 @@ const SelectInput = (props: SelectInputProps) => { className={cn('w-full', className?.select)} classNames={{ control: ({ isFocused, isDisabled }) => - cn( - 'w-full border transition-shadow', - // Gunakan rounded-lg untuk semua kasus - 'rounded-lg!', - { - 'bg-base-100!': !isDisabled && !readOnly, - 'bg-base-200! text-gray-400 cursor-not-allowed': - isDisabled && !readOnly, - 'bg-transparent! cursor-not-allowed!': readOnly, - 'cursor-pointer!': !readOnly && !isDisabled, - 'border-red-500! ring-2 ring-red-200': isError, - 'border-indigo-500 ring-2 ring-indigo-200': - isFocused && !startAdornment, - 'border-base-content/10!': !isError && !isFocused, - } - ), + cn('w-full border transition-shadow rounded-lg!', { + 'bg-base-100!': !isDisabled && !readOnly, + 'bg-base-200! text-gray-400 cursor-not-allowed': + isDisabled && !readOnly, + 'bg-transparent! cursor-not-allowed!': readOnly, + 'cursor-pointer!': !readOnly && !isDisabled, + 'border-error!': isError, + 'ring-2 ring-error/20': isError, + 'border-indigo-500 ring-2 ring-indigo-200': + isFocused && !startAdornment && !isError, + 'border-base-content/10!': !isError && !isFocused, + }), valueContainer: () => cn('flex-1 px-3! pr-2! py-2.5! gap-1'), placeholder: () => - cn({ - 'text-gray-400 text-sm leading-tight': !isError, - 'text-red-300!': isError, + cn('text-gray-400 text-sm leading-tight', { + 'text-error!': isError, }), singleValue: () => - cn({ - 'm-0! text-gray-900 text-sm leading-tight': !isError, - 'text-error!': isError, + cn('m-0! text-gray-900 text-sm leading-tight', { + 'text-error!': isError && !readOnly, 'text-gray-900!': readOnly, }), input: () => cn('text-gray-900 m-0! p-0! text-sm leading-tight'), From 3a676723e41afb8c92fb091c90b5553b352156d7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:42:04 +0700 Subject: [PATCH 090/149] refactor(FE): Refactor table class names in DailyMarketingTab --- .../marketing/tab/DailyMarketingTab.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index df06c83c..a336b671 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -725,10 +725,21 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { columns={getTableColumns()} renderFooter={data.length > 0} className={{ - containerClassName: cn('p-3', { - 'w-full mb-20': data.length === 0, - }), - headerColumnClassName: 'text-nowrap', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200 text-nowrap', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} /> )} From d312da4c6639beb41fb866eea56a9be29988b9f9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:53:33 +0700 Subject: [PATCH 091/149] refactor(FE): Refactor dropdown and export button components --- .../report/finance/tab/CustomerPaymentTab.tsx | 77 +++++++++++-------- .../report/finance/tab/DebtSupplierTab.tsx | 69 ++++++++++------- .../tab/PurchasesPerSupplierTab.tsx | 77 +++++++++++-------- .../report/marketing/tab/HppPerKandangTab.tsx | 77 +++++++++++-------- .../tab/ProductionResultTab.tsx | 77 +++++++++++-------- 5 files changed, 216 insertions(+), 161 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 1443fa0b..1c546058 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -25,8 +25,6 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; import Modal, { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; import { useFormik } from 'formik'; @@ -387,13 +385,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { color='none' onClick={handleFilterModalOpen} className={cn( - 'px-3 py-2.5', - 'rounded-lg! font-semibold text-sm gap-1.5', - 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', - hasFilters && 'border-primary-gradient text-primary' + 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', + { + 'border-primary-gradient text-primary': hasFilters, + } )} > - + Filter {hasFilters && ( @@ -403,42 +401,55 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 44677313..d0a27b92 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -3,8 +3,6 @@ import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; import { OptionType, useSelect } from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -277,7 +275,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { useEffect(() => { setTabActions( tabId, -
+
{ /> - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 5e2c4e27..36476956 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -3,8 +3,6 @@ import Card from '@/components/Card'; import Dropdown from '@/components/Dropdown'; import DateInput from '@/components/input/DateInput'; import { useSelect } from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; import Table from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -498,13 +496,13 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { color='none' onClick={handleFilterModalOpen} className={cn( - 'px-3 py-2.5', - 'rounded-lg! font-semibold text-sm gap-1.5', - 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', - hasFilters && 'border-primary-gradient text-primary' + 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', + { + 'border-primary-gradient text-primary': hasFilters, + } )} > - + Filter {hasFilters && ( @@ -514,42 +512,55 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index d3d4bfac..e106dbf4 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -18,8 +18,6 @@ import { import { isResponseSuccess } from '@/lib/api-helper'; import Button from '@/components/Button'; import Dropdown from '@/components/Dropdown'; -import MenuItem from '@/components/menu/MenuItem'; -import Menu from '@/components/menu/Menu'; import { generateHppPerKandangPDF } from '@/components/pages/report/marketing/export/HppPerkandangExportPDF'; import { generateHppPerKandangExcel } from '@/components/pages/report/marketing/export/HppPerkandangExportXLSX'; import toast from 'react-hot-toast'; @@ -499,13 +497,13 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { color='none' onClick={handleFilterModalOpen} className={cn( - 'px-3 py-2.5', - 'rounded-lg! font-semibold text-sm gap-1.5', - 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', - hasFilters && 'border-primary-gradient text-primary' + 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', + { + 'border-primary-gradient text-primary': hasFilters, + } )} > - + Filter {hasFilters && ( @@ -515,42 +513,55 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx index 090ca781..d04c9e20 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultTab.tsx @@ -9,8 +9,6 @@ import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Dropdown from '@/components/dropdown/Dropdown'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable'; import { useFormik } from 'formik'; import { @@ -546,13 +544,13 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { color='none' onClick={() => filterModal.openModal()} className={cn( - 'px-3 py-2.5', - 'rounded-lg! font-semibold text-sm gap-1.5', - 'text-sm text-base-content/50 border border-base-content/10 shadow-button-soft', - hasFilters && 'border-primary-gradient text-primary' + 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', + { + 'border-primary-gradient text-primary': hasFilters, + } )} > - + Filter {hasFilters && ( @@ -562,42 +560,55 @@ const ProductionResultContent = ({ tabId }: ProductionResultTabProps) => { - - Export -
- +
+ + + Export + +
+ +
} - align='end' - className={{ - content: - 'mt-1 p-0 w-full shadow-button-soft border border-base-content/10 rounded-lg', - }} > - - - - + +
); From ceb594a4ccd871bb9214615faf74561a9a67a7f9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 09:55:22 +0700 Subject: [PATCH 092/149] refactor(FE): Rename and update paths for ProductionResult components --- .../pages/report/production-result/ProductionResultTabs.tsx | 2 +- ...ResultTab.tsx => ProductionResultProjectFlockKandangTab.tsx} | 2 +- .../{ => tab}/ProductionResultProjectFlockKandangTable.tsx | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/components/pages/report/production-result/tab/{ProductionResultTab.tsx => ProductionResultProjectFlockKandangTab.tsx} (99%) rename src/components/pages/report/production-result/{ => tab}/ProductionResultProjectFlockKandangTable.tsx (100%) diff --git a/src/components/pages/report/production-result/ProductionResultTabs.tsx b/src/components/pages/report/production-result/ProductionResultTabs.tsx index 2bd7765f..6f5e4410 100644 --- a/src/components/pages/report/production-result/ProductionResultTabs.tsx +++ b/src/components/pages/report/production-result/ProductionResultTabs.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import Tabs from '@/components/Tabs'; -import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultTab'; +import ProductionResultTab from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab'; import { useReportTabStore } from '@/stores/report/report-tab.store'; const ProductionResultTabs = () => { diff --git a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx similarity index 99% rename from src/components/pages/report/production-result/tab/ProductionResultTab.tsx rename to src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx index d04c9e20..9ac5faf6 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultTab.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTab.tsx @@ -9,7 +9,7 @@ import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Dropdown from '@/components/dropdown/Dropdown'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; -import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/ProductionResultProjectFlockKandangTable'; +import ProductionResultProjectFlockKandangTable from '@/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable'; import { useFormik } from 'formik'; import { ProductionResultFilterSchema, diff --git a/src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx similarity index 100% rename from src/components/pages/report/production-result/ProductionResultProjectFlockKandangTable.tsx rename to src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx From 67f2a80f237df64d4c3c51b8eb251ddef7da9fe0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 10:19:09 +0700 Subject: [PATCH 093/149] refactor(FE): Refactor expense report page to use tab-based layout --- src/app/report/expense/page.tsx | 8 +- .../report/expense/ReportExpenseTable.tsx | 901 ------------------ .../report/expense/ReportExpenseTabs.tsx | 40 + .../ReportExpenseExportPDF.tsx} | 1 + .../export/ReportExpenseExportXLSX.tsx | 109 +++ .../expense/filter/ReportExpenseFilter.ts | 73 ++ .../pdf/styles/ReportExpenseStyles.tsx | 365 ------- .../skeleton/ReportExpenseSkeleton.tsx | 51 + .../report/expense/tab/ReportExpenseTab.tsx | 755 +++++++++++++++ 9 files changed, 1031 insertions(+), 1272 deletions(-) delete mode 100644 src/components/pages/report/expense/ReportExpenseTable.tsx create mode 100644 src/components/pages/report/expense/ReportExpenseTabs.tsx rename src/components/pages/report/expense/{pdf/ReportExpenseExport.tsx => export/ReportExpenseExportPDF.tsx} (99%) create mode 100644 src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx create mode 100644 src/components/pages/report/expense/filter/ReportExpenseFilter.ts delete mode 100644 src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx create mode 100644 src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx create mode 100644 src/components/pages/report/expense/tab/ReportExpenseTab.tsx diff --git a/src/app/report/expense/page.tsx b/src/app/report/expense/page.tsx index 99d2862e..bb497283 100644 --- a/src/app/report/expense/page.tsx +++ b/src/app/report/expense/page.tsx @@ -1,13 +1,9 @@ 'use client'; -import ReportExpenseTable from '@/components/pages/report/expense/ReportExpenseTable'; +import ReportExpenseTabs from '@/components/pages/report/expense/ReportExpenseTabs'; const ReportExpense = () => { - return ( -
- -
- ); + return ; }; export default ReportExpense; diff --git a/src/components/pages/report/expense/ReportExpenseTable.tsx b/src/components/pages/report/expense/ReportExpenseTable.tsx deleted file mode 100644 index c809c153..00000000 --- a/src/components/pages/report/expense/ReportExpenseTable.tsx +++ /dev/null @@ -1,901 +0,0 @@ -import { useState, useMemo, useCallback } from 'react'; -import { ChangeEventHandler } from 'react'; -import useSWR from 'swr'; -import Button from '@/components/Button'; -import Card from '@/components/Card'; -import DateInput from '@/components/input/DateInput'; -import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; -import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; -import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; -import { ReportExpense } from '@/types/api/report/report-expense'; -import { Icon } from '@iconify/react'; -import { ColumnDef } from '@tanstack/react-table'; -import { ReportExpenseApi } from '@/services/api/report'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import Pagination from '@/components/Pagination'; -import Dropdown from '@/components/dropdown/Dropdown'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; -import * as XLSX from 'xlsx'; -import { generateReportExpensePDF } from './pdf/ReportExpenseExport'; -import toast from 'react-hot-toast'; -import { - KandangApi, - LocationApi, - NonstockApi, - SupplierApi, -} from '@/services/api/master-data'; -import { Supplier } from '@/types/api/master-data/supplier'; -import { Kandang } from '@/types/api/master-data/kandang'; -import { Nonstock } from '@/types/api/master-data/nonstock'; - -const ReportExpenseTable = () => { - // ===== STATE MANAGEMENT ===== - const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); - const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); - const [dropdownOpen, setDropdownOpen] = useState(false); - const [pdfProgress, setPdfProgress] = useState(0); - const [excelProgress, setExcelProgress] = useState(0); - const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; - - // ===== SUBMISSION STATE ===== - const [isSubmitted, setIsSubmitted] = useState(false); - - // ===== TABLE FILTER STATE ===== - const { - state: filterState, - updateFilter, - setPage, - setPageSize, - reset: resetFilterState, - toQueryString, - } = useTableFilter({ - initial: { - location_id: '', - supplier_id: '', - kandang_id: '', - nonstock_id: '', - realization_date: '', - category: '', - search: '', - }, - paramMap: { - page: 'page', - pageSize: 'limit', - }, - }); - - // ===== SELECT OPTIONS ===== - const { - setInputValue: setLocationInputValue, - options: locationOptions, - isLoadingOptions: isLoadingLocationOptions, - loadMore: loadMoreLocations, - } = useSelect(LocationApi.basePath, 'id', 'name'); - - const { - setInputValue: setSupplierInputValue, - options: supplierOptions, - isLoadingOptions: isLoadingSupplierOptions, - loadMore: loadMoreSuppliers, - } = useSelect(SupplierApi.basePath, 'id', 'name'); - - const { - setInputValue: setKandangInputValue, - options: kandangOptions, - isLoadingOptions: isLoadingKandangOptions, - loadMore: loadMoreKandangs, - } = useSelect(KandangApi.basePath, 'id', 'name'); - - const { - setInputValue: setNonstockInputValue, - options: nonstockOptions, - isLoadingOptions: isLoadingNonstockOptions, - loadMore: loadMoreNonstocks, - } = useSelect(NonstockApi.basePath, 'id', 'name'); - - const categoryOptions = useMemo( - () => [ - { value: 'BOP', label: 'BOP' }, - { value: 'NON-BOP', label: 'Non BOP' }, - ], - [] - ); - - // Mendapatkan value option select dari filter state - const selectedLocation = useMemo( - () => - locationOptions.find( - (opt) => String(opt.value) === filterState.location_id - ) || null, - [locationOptions, filterState.location_id] - ); - const selectedSupplier = useMemo( - () => - supplierOptions.find( - (opt) => String(opt.value) === filterState.supplier_id - ) || null, - [supplierOptions, filterState.supplier_id] - ); - const selectedKandang = useMemo( - () => - kandangOptions.find( - (opt) => String(opt.value) === filterState.kandang_id - ) || null, - [kandangOptions, filterState.kandang_id] - ); - const selectedNonstock = useMemo( - () => - nonstockOptions.find( - (opt) => String(opt.value) === filterState.nonstock_id - ) || null, - [nonstockOptions, filterState.nonstock_id] - ); - const selectedCategory = useMemo( - () => - categoryOptions.find((opt) => opt.value === filterState.category) || null, - [categoryOptions, filterState.category] - ); - - // ===== FILTER CHANGE HANDLERS ===== - const locationChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('location_id', option ? String(option.value) : ''); - updateFilter('kandang_id', ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const kandangChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('kandang_id', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const supplierChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('supplier_id', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const nonstockChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('nonstock_id', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const categoryChangeHandler = useCallback( - (val: OptionType | OptionType[] | null) => { - const option = val as OptionType; - updateFilter('category', option ? String(option.value) : ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const realizationDateChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - updateFilter('realization_date', e.target.value || ''); - setIsSubmitted(false); - }, - [updateFilter] - ); - - const searchChangeHandler = useCallback( - (e: React.ChangeEvent) => { - updateFilter('search', e.target.value); - setIsSubmitted(false); - }, - [updateFilter] - ); - - // ===== RESET FILTERS ===== - const resetFilters = useCallback(() => { - resetFilterState(); - setIsSubmitted(false); - }, [resetFilterState]); - - // ===== SUBMIT HANDLER ===== - const handleSubmit = useCallback(() => { - setIsSubmitted(true); - setPage(1); - }, [setPage]); - - // ===== DATA FETCHING FOR TABLE ===== - const { data: reportExpenseResponse, isLoading } = useSWR( - isSubmitted - ? () => { - return ['report-expense', toQueryString()]; - } - : null, - ([, query]) => { - const endpoint = `${ReportExpenseApi.basePath}${query}`; - return ReportExpenseApi.getAllFetcher(endpoint); - } - ); - - const data: ReportExpense[] = useMemo( - () => - isResponseSuccess(reportExpenseResponse) - ? (reportExpenseResponse?.data as ReportExpense[]) || [] - : [], - [reportExpenseResponse] - ); - - const meta = useMemo( - () => - isResponseSuccess(reportExpenseResponse) && reportExpenseResponse.meta - ? reportExpenseResponse.meta - : null, - [reportExpenseResponse] - ); - - // ===== EXPORT DATA FETCHER ===== - const reportExpenseExport = useCallback(async (): Promise< - ReportExpense[] | null - > => { - const params = new URLSearchParams(toQueryString().replace('?', '')); - params.set('limit', 'limit'); - params.set('page', '1'); - - const endpoint = `${ReportExpenseApi.basePath}?${params.toString()}`; - const response = await ReportExpenseApi.getAllFetcher(endpoint); - - return isResponseSuccess(response) ? response.data : null; - }, [toQueryString]); - - // ===== EXPORT HANDLERS ===== - const handleExportPdf = useCallback(async () => { - if (isPdfExportLoading) return; - setIsPdfExportLoading(true); - setPdfProgress(0); - - await new Promise((resolve) => - requestAnimationFrame(() => resolve(undefined)) - ); - - try { - // Stage 1: Fetching data (0-20%) - setPdfProgress(10); - await new Promise((resolve) => setTimeout(resolve, 50)); - - const allData = await reportExpenseExport(); - if (!allData || allData.length === 0) { - toast.error('Tidak ada data untuk diekspor.'); - setIsPdfExportLoading(false); - setPdfProgress(0); - return; - } - - // Stage 2: Data fetched - langsung loncat ke progress tinggi - setPdfProgress(30); - await new Promise((resolve) => setTimeout(resolve, 50)); - const progressInterval = setInterval(() => { - setPdfProgress((prev) => { - // Increment kecil dan random antara 0.5-2% - const increment = Math.random() * 1.5 + 0.5; - const newProgress = Math.min(prev + increment, 50); - return newProgress; - }); - }, 300); // Update setiap 300ms - - const pdfParams = { - location_name: selectedLocation?.label, - supplier_name: selectedSupplier?.label, - kandang_name: selectedKandang?.label, - nonstock_name: selectedNonstock?.label, - category: selectedCategory?.label, - realization_date: filterState.realization_date, - search: filterState.search, - }; - - setDropdownOpen(false); - - // Stage 3: Langsung loncat ke 80-85% untuk menghindari stuck - const baseProgress = 80 + Math.floor(Math.random() * 16); // Random 80-85% - setPdfProgress(baseProgress); - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Stage 4: Berikan jeda untuk UI update - await new Promise((resolve) => - requestAnimationFrame(() => resolve(undefined)) - ); - - // Proses PDF yang sebenarnya - await generateReportExpensePDF(allData, pdfParams); - - clearInterval(progressInterval); - - // Stage 5: Finalizing (98-100%) - setPdfProgress(99); - await new Promise((resolve) => setTimeout(resolve, 100)); - - setPdfProgress(100); - toast.success('PDF berhasil dibuat dan diunduh.'); - - // Reset progress setelah selesai - setTimeout(() => setPdfProgress(0), 500); - } catch (error) { - console.error('PDF Export Error:', error); - toast.error('Gagal membuat PDF. Silakan coba lagi.'); - setPdfProgress(0); - } finally { - setIsPdfExportLoading(false); - } - }, [ - reportExpenseExport, - selectedLocation, - selectedSupplier, - selectedKandang, - selectedNonstock, - selectedCategory, - filterState.realization_date, - filterState.search, - ]); - - const handleExportExcel = useCallback(async () => { - if (isExcelExportLoading) return; - setIsExcelExportLoading(true); - setExcelProgress(0); - setDropdownOpen(false); - - await new Promise((resolve) => - requestAnimationFrame(() => resolve(undefined)) - ); - - try { - // Stage 1: Fetching data (0-20%) - setExcelProgress(15); - await new Promise((resolve) => setTimeout(resolve, 50)); - - const allDataForExport = await reportExpenseExport(); - - if (!allDataForExport || allDataForExport.length === 0) { - toast.error('Tidak ada data untuk diekspor.'); - setIsExcelExportLoading(false); - setExcelProgress(0); - return; - } - - // Stage 2: Data fetched (20-40%) - setExcelProgress(30); - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Stage 3: Grouping data (40-60%) - setExcelProgress(50); - const groupedBySupplier: Record = {}; - allDataForExport.forEach((item) => { - const supplierName = item.supplier?.name || 'Unknown Supplier'; - if (!groupedBySupplier[supplierName]) { - groupedBySupplier[supplierName] = []; - } - groupedBySupplier[supplierName].push(item); - }); - - await new Promise((resolve) => setTimeout(resolve, 50)); - - // Stage 4: Creating workbook (60-80%) - setExcelProgress(70); - const workbook = XLSX.utils.book_new(); - - const supplierEntries = Object.entries(groupedBySupplier); - const totalSuppliers = supplierEntries.length; - - for (let i = 0; i < supplierEntries.length; i++) { - const [supplierName, supplierData] = supplierEntries[i]; - - // Update progress per supplier - const progressIncrement = (20 / totalSuppliers) * (i + 1); - setExcelProgress(70 + progressIncrement); - - const totals = supplierData.reduce( - (acc, item) => ({ - qty_pengajuan: acc.qty_pengajuan + (item.pengajuan?.qty || 0), - total_pengajuan: - acc.total_pengajuan + - (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), - qty_realisasi: acc.qty_realisasi + (item.realisasi?.qty || 0), - total_realisasi: - acc.total_realisasi + - (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), - }), - { - qty_pengajuan: 0, - total_pengajuan: 0, - qty_realisasi: 0, - total_realisasi: 0, - } - ); - - const excelData = supplierData.map((item, index) => ({ - No: index + 1, - 'No. PO': item.po_number || '', - 'No. Referensi': item.reference_number || '', - 'Tanggal Realisasi': item.realization_date - ? formatDate(item.realization_date, 'DD MMM YYYY') - : '', - 'Tanggal Transaksi': item.transaction_date - ? formatDate(item.transaction_date, 'DD MMM YYYY') - : '', - Kategori: item.category || '', - Produk: item.pengajuan?.nonstock?.name || '', - Lokasi: item.kandang?.location?.name || '', - Kandang: item.kandang?.name || '', - 'Qty Pengajuan': item.pengajuan?.qty || 0, - 'Harga Pengajuan': item.pengajuan?.price || 0, - 'Total Pengajuan': - (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), - 'Qty Realisasi': item.realisasi?.qty || 0, - 'Harga Realisasi': item.realisasi?.price || 0, - 'Total Realisasi': - (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), - 'Status Pencairan': item.latest_approval?.step_name || '', - })); - - excelData.push({ - No: 'Total' as unknown as number, - 'No. PO': '', - 'No. Referensi': '', - 'Tanggal Realisasi': '', - 'Tanggal Transaksi': '', - Kategori: '', - Produk: '', - Lokasi: '', - Kandang: '', - 'Qty Pengajuan': totals.qty_pengajuan, - 'Harga Pengajuan': 0, - 'Total Pengajuan': totals.total_pengajuan, - 'Qty Realisasi': totals.qty_realisasi, - 'Harga Realisasi': 0, - 'Total Realisasi': totals.total_realisasi, - 'Status Pencairan': '', - }); - - const worksheet = XLSX.utils.json_to_sheet(excelData); - const colWidths = [ - { wch: 5 }, // No - { wch: 20 }, // No. PO - { wch: 20 }, // No. Referensi - { wch: 15 }, // Tanggal Realisasi - { wch: 15 }, // Tanggal Transaksi - { wch: 15 }, // Kategori - { wch: 30 }, // Produk - { wch: 20 }, // Lokasi - { wch: 15 }, // Kandang - { wch: 15 }, // Qty Pengajuan - { wch: 15 }, // Harga Pengajuan - { wch: 20 }, // Total Pengajuan - { wch: 15 }, // Qty Realisasi - { wch: 15 }, // Harga Realisasi - { wch: 20 }, // Total Realisasi - { wch: 20 }, // Status Pencairan - ]; - worksheet['!cols'] = colWidths; - - const sheetName = supplierName.slice(0, 31); - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); - - // Small delay to allow UI update - if (i < supplierEntries.length - 1) { - await new Promise((resolve) => setTimeout(resolve, 10)); - } - } - - // Stage 5: Writing file (90-100%) - setExcelProgress(95); - await new Promise((resolve) => setTimeout(resolve, 50)); - - const filename = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; - XLSX.writeFile(workbook, filename); - - setExcelProgress(100); - toast.success('Excel berhasil dibuat dan diunduh.'); - - // Reset progress - setTimeout(() => setExcelProgress(0), 500); - } catch (error) { - console.error('Excel Export Error:', error); - toast.error('Gagal membuat Excel. Silakan coba lagi.'); - setExcelProgress(0); - } finally { - setIsExcelExportLoading(false); - } - }, [isExcelExportLoading, reportExpenseExport]); - - // ===== PAGINATION HANDLERS ===== - const handlePageChange = (page: number) => { - setPage(page); - }; - - const handleRowChange = (pageSize: number) => { - setPageSize(pageSize); - }; - - const handleNextPage = () => { - if (meta && filterState.page < meta.total_pages) { - setPage(filterState.page + 1); - } - }; - - const handlePrevPage = () => { - if (filterState.page > 1) { - setPage(filterState.page - 1); - } - }; - - // ===== TABLE COLUMNS DEFINITION ===== - const columns = useMemo((): ColumnDef[] => { - return [ - { - header: 'No', - accessorFn: (_, index) => - (filterState.page - 1) * filterState.pageSize + index + 1, - }, - { - header: 'No. PO', - accessorKey: 'po_number', - }, - { - header: 'No. Referensi', - accessorKey: 'reference_number', - }, - { - header: 'Tanggal Realisasi', - accessorKey: 'realization_date', - cell: ({ row }) => { - return formatDate(row.original?.realization_date, 'DD MMM, YYYY'); - }, - }, - { - header: 'Tanggal Transaksi', - accessorKey: 'transaction_date', - cell: ({ row }) => { - return formatDate(row.original?.transaction_date, 'DD MMM, YYYY'); - }, - }, - { - header: 'Kategori', - accessorKey: 'category', - }, - { - header: 'Produk', - accessorFn: (row) => row.pengajuan?.nonstock?.name, - }, - { - header: 'Supplier', - accessorFn: (row) => row.supplier?.name, - }, - { - header: 'Lokasi', - accessorFn: (row) => row.kandang?.location?.name, - }, - { - header: 'Kandang', - accessorFn: (row) => row.kandang?.name, - }, - { - header: 'Pengajuan', - columns: [ - { - header: 'Qty', - id: 'qty_pengajuan', - accessorFn: (row) => row.pengajuan?.qty, - cell: ({ row }) => - row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0', - }, - { - header: 'Harga', - id: 'harga_pengajuan', - accessorFn: (row) => row.pengajuan?.price, - cell: ({ row }) => - formatCurrency(row.original.pengajuan?.price || 0), - }, - { - header: 'Total', - id: 'total_pengajuan', - accessorFn: (row) => - (row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0), - cell: ({ row }) => { - const total = - (row.original.pengajuan?.qty || 0) * - (row.original.pengajuan?.price || 0); - return formatCurrency(total); - }, - }, - ], - }, - { - header: 'Realisasi', - columns: [ - { - header: 'Qty', - id: 'qty_realisasi', - accessorFn: (row) => row.realisasi?.qty, - cell: ({ row }) => - row.original.realisasi?.qty?.toLocaleString('id-ID') || '0', - }, - { - header: 'Harga', - id: 'harga_realisasi', - accessorFn: (row) => row.realisasi?.price, - cell: ({ row }) => - formatCurrency(row.original.realisasi?.price || 0), - }, - { - header: 'Total', - id: 'total_realisasi', - accessorFn: (row) => - (row.realisasi?.qty || 0) * (row.realisasi?.price || 0), - cell: ({ row }) => { - const total = - (row.original.realisasi?.qty || 0) * - (row.original.realisasi?.price || 0); - return formatCurrency(total); - }, - }, - ], - }, - { - header: 'Status Pencairan', - cell: (props) => ( - - ), - }, - { - header: 'Status BOP', - cell: (props) => ( - - ), - }, - ]; - }, [filterState.page, filterState.pageSize]); - - // ===== RENDER ===== - return ( -
- {isAnyExportLoading && ( -
- - {((isPdfExportLoading && pdfProgress > 0) || - (isExcelExportLoading && excelProgress > 0)) && ( -
-
- {(() => { - const currentProgress = isPdfExportLoading - ? pdfProgress - : excelProgress; - const exportType = isPdfExportLoading ? 'PDF' : 'Excel'; - - if (currentProgress < 20) - return 'Mengambil data dari server...'; - if (currentProgress < 30) return 'Memproses data laporan...'; - if (currentProgress < 40) - return `Menyiapkan struktur dokumen ${exportType}...`; - if (currentProgress < 50) - return 'Mengelompokkan data per supplier...'; - if (currentProgress < 70) - return 'Merender tabel dan kalkulasi...'; - if (currentProgress < 96) - return `Memformat dokumen ${exportType}...`; - if (currentProgress < 100) - return 'Menyelesaikan dan mengunduh...'; - return 'Selesai!'; - })()}{' '} - {Math.round(isPdfExportLoading ? pdfProgress : excelProgress)}% -
- {((isPdfExportLoading && pdfProgress >= 35 && pdfProgress < 90) || - (isExcelExportLoading && - excelProgress >= 35 && - excelProgress < 90)) && ( -
- {(isPdfExportLoading ? pdfProgress : excelProgress) < 96 - ? 'Proses ini membutuhkan waktu lebih lama untuk data dalam jumlah besar. Mohon bersabar...' - : 'Sedang memproses baris data. Hampir selesai...'} -
- )} -
- )} -
- )} - -
-
- - -
-
- { - setDropdownOpen(!dropdownOpen); - }} - > - Export - - } - align='end' - direction='bottom' - open={dropdownOpen} - > - - - - - -
-
-
- } - > -
- - - - - - - } - /> -
- - - {/* ===== TABLE CONTENT ===== */} - {!isSubmitted ? ( -
- Silakan pilih filter dan klik tombol Cari untuk menampilkan data. -
- ) : isLoading ? ( -
- -
- ) : data.length === 0 ? ( -
- Tidak ada data yang dapat ditampilkan... -
- ) : ( - <> - - columns={columns} - data={data} - pageSize={10} - className={{ - containerClassName: 'mb-0', - headerRowClassName: cn( - TABLE_DEFAULT_STYLING, - 'whitespace-nowrap' - ), - bodyRowClassName: cn(TABLE_DEFAULT_STYLING, 'whitespace-nowrap'), - paginationClassName: 'hidden', - }} - /> - {meta && ( -
- -
- )} - - )} -
- ); -}; - -export default ReportExpenseTable; diff --git a/src/components/pages/report/expense/ReportExpenseTabs.tsx b/src/components/pages/report/expense/ReportExpenseTabs.tsx new file mode 100644 index 00000000..704d1f6f --- /dev/null +++ b/src/components/pages/report/expense/ReportExpenseTabs.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useState } from 'react'; +import Tabs from '@/components/Tabs'; + +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import ReportExpenseTab from './tab/ReportExpenseTab'; + +const ReportExpenseTabs = () => { + const [activeTabId, setActiveTabId] = useState('1'); + const tabActions = useReportTabStore((state) => state.tabActions); + + const tabs = [ + { + id: '1', + label: 'Laporan Biaya Operasional', + content: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default ReportExpenseTabs; diff --git a/src/components/pages/report/expense/pdf/ReportExpenseExport.tsx b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx similarity index 99% rename from src/components/pages/report/expense/pdf/ReportExpenseExport.tsx rename to src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx index a7ff8599..6ec2c559 100644 --- a/src/components/pages/report/expense/pdf/ReportExpenseExport.tsx +++ b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx @@ -2,6 +2,7 @@ import { ReportExpense } from '@/types/api/report/report-expense'; import { formatCurrency, formatDate } from '@/lib/helper'; import jsPDF from 'jspdf'; import autoTable, { UserOptions } from 'jspdf-autotable'; + interface jsPDFWithAutoTable extends jsPDF { lastAutoTable: { finalY: number; diff --git a/src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx b/src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx new file mode 100644 index 00000000..cc27b526 --- /dev/null +++ b/src/components/pages/report/expense/export/ReportExpenseExportXLSX.tsx @@ -0,0 +1,109 @@ +import * as XLSX from 'xlsx'; +import { ReportExpense } from '@/types/api/report/report-expense'; +import { formatCurrency, formatDate } from '@/lib/helper'; + +export const generateReportExpenseExcel = async ( + data: ReportExpense[] +): Promise => { + // Group by supplier + const groupedBySupplier: Record = {}; + data.forEach((item) => { + const supplierName = item.supplier?.name || 'Unknown Supplier'; + if (!groupedBySupplier[supplierName]) { + groupedBySupplier[supplierName] = []; + } + groupedBySupplier[supplierName].push(item); + }); + + const workbook = XLSX.utils.book_new(); + + Object.entries(groupedBySupplier).forEach(([supplierName, supplierData]) => { + const totals = supplierData.reduce( + (acc, item) => ({ + qty_pengajuan: acc.qty_pengajuan + (item.pengajuan?.qty || 0), + total_pengajuan: + acc.total_pengajuan + + (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), + qty_realisasi: acc.qty_realisasi + (item.realisasi?.qty || 0), + total_realisasi: + acc.total_realisasi + + (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), + }), + { + qty_pengajuan: 0, + total_pengajuan: 0, + qty_realisasi: 0, + total_realisasi: 0, + } + ); + + const excelData = supplierData.map((item, index) => ({ + No: index + 1, + 'No. PO': item.po_number || '', + 'No. Referensi': item.reference_number || '', + 'Tanggal Realisasi': item.realization_date + ? formatDate(item.realization_date, 'DD MMM YYYY') + : '', + 'Tanggal Transaksi': item.transaction_date + ? formatDate(item.transaction_date, 'DD MMM YYYY') + : '', + Kategori: item.category || '', + Produk: item.pengajuan?.nonstock?.name || '', + Lokasi: item.kandang?.location?.name || '', + Kandang: item.kandang?.name || '', + 'Qty Pengajuan': item.pengajuan?.qty || 0, + 'Harga Pengajuan': item.pengajuan?.price || 0, + 'Total Pengajuan': + (item.pengajuan?.qty || 0) * (item.pengajuan?.price || 0), + 'Qty Realisasi': item.realisasi?.qty || 0, + 'Harga Realisasi': item.realisasi?.price || 0, + 'Total Realisasi': + (item.realisasi?.qty || 0) * (item.realisasi?.price || 0), + 'Status Pencairan': item.latest_approval?.step_name || '', + })); + + excelData.push({ + No: 'Total' as unknown as number, + 'No. PO': '', + 'No. Referensi': '', + 'Tanggal Realisasi': '', + 'Tanggal Transaksi': '', + Kategori: '', + Produk: '', + Lokasi: '', + Kandang: '', + 'Qty Pengajuan': totals.qty_pengajuan, + 'Harga Pengajuan': 0, + 'Total Pengajuan': totals.total_pengajuan, + 'Qty Realisasi': totals.qty_realisasi, + 'Harga Realisasi': 0, + 'Total Realisasi': totals.total_realisasi, + 'Status Pencairan': '', + }); + + const worksheet = XLSX.utils.json_to_sheet(excelData); + const colWidths = [ + { wch: 5 }, + { wch: 20 }, + { wch: 20 }, + { wch: 15 }, + { wch: 15 }, + { wch: 15 }, + { wch: 30 }, + { wch: 20 }, + { wch: 15 }, + { wch: 15 }, + { wch: 15 }, + { wch: 20 }, + { wch: 15 }, + { wch: 20 }, + ]; + worksheet['!cols'] = colWidths; + + const sheetName = supplierName.slice(0, 31); + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + const filename = `Laporan-BOP-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + XLSX.writeFile(workbook, filename); +}; diff --git a/src/components/pages/report/expense/filter/ReportExpenseFilter.ts b/src/components/pages/report/expense/filter/ReportExpenseFilter.ts new file mode 100644 index 00000000..b8bd3c56 --- /dev/null +++ b/src/components/pages/report/expense/filter/ReportExpenseFilter.ts @@ -0,0 +1,73 @@ +import { OptionType } from '@/components/input/SelectInput'; +import * as yup from 'yup'; + +export type ReportExpenseFilterProps = { + location_id: string | null; + supplier_id: string | null; + kandang_id: string | null; + nonstock_id: string | null; + realization_date: string | null; + category: string | null; +}; + +export type ReportExpenseFilterFormType = { + location_id: OptionType | null; + supplier_id: OptionType | null; + kandang_id: OptionType | null; + nonstock_id: OptionType | null; + realization_date: string | null; + category: OptionType | null; +}; + +export const ReportExpenseFilterSchema = yup.object({ + location_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Lokasi wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + supplier_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Supplier wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + kandang_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Kandang wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + nonstock_id: yup + .mixed() + .nullable() + .test('is-not-empty', 'Produk wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), + realization_date: yup.string().nullable(), + category: yup + .mixed() + .nullable() + .test('is-not-empty', 'Kategori wajib dipilih', (value) => { + if (Array.isArray(value)) { + return value.length > 0; + } + return true; + }), +}) as yup.ObjectSchema; + +export type ReportExpenseFilterValues = yup.InferType< + typeof ReportExpenseFilterSchema +>; diff --git a/src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx b/src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx deleted file mode 100644 index 65505a5f..00000000 --- a/src/components/pages/report/expense/pdf/styles/ReportExpenseStyles.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import { StyleSheet } from '@react-pdf/renderer'; - -const pdfStyles = StyleSheet.create({ - page: { - fontSize: 18, - fontFamily: 'Helvetica', - padding: 20, - backgroundColor: '#FFFFFF', - }, - header: { - marginBottom: 20, - }, - logo: { - width: 120, - height: 30, - marginBottom: 8, - }, - companyInfo: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 4, - color: '#1f74bf', - }, - address: { - fontSize: 7, - color: '#666666', - maxWidth: 400, - marginBottom: 10, - }, - divider: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - marginBottom: 15, - }, - titleSection: { - flexDirection: 'row', - marginBottom: 20, - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - title: { - fontSize: 18, - fontWeight: 'bold', - flex: 3, - color: '#1f74bf', - }, - poInfo: { - flex: 1, - fontSize: 7, - textAlign: 'right', - }, - sectionTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 8, - color: '#1f74bf', - }, - table: { - borderWidth: 1, - borderColor: '#000000', - marginBottom: 15, - }, - tableRow: { - flexDirection: 'row', - }, - tableHeader: { - backgroundColor: '#F5F5F5', - }, - tableCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellLast: { - flex: 1, - padding: 3, - fontSize: 7, - }, - tableCellHeader: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellHeaderLast: { - flex: 1, - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellRightLast: { - flex: 1, - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellNarrow: { - width: '1%', - minWidth: 20, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'center', - }, - tableCellNarrowHeader: { - width: '1%', - minWidth: 20, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - textAlign: 'center', - }, - tableCellWrap: { - flex: 1, - maxWidth: 80, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - flexWrap: 'wrap', - }, - tableCellWrapHeader: { - flex: 1, - maxWidth: 80, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - // Nested header styles - tableHeaderGroup: { - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - backgroundColor: '#F5F5F5', - }, - tableHeaderGroupLast: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - backgroundColor: '#F5F5F5', - }, - tableHeaderGroupTitle: { - padding: 3, - fontSize: 7, - fontWeight: 'bold', - textAlign: 'center', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - tableSubHeaderRow: { - flexDirection: 'row', - }, - // Specific width columns - tableCellXSmall: { - width: 30, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellXSmallHeader: { - width: 30, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellSmall: { - width: 40, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellSmallHeader: { - width: 40, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellMedium: { - width: 60, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - tableCellMediumHeader: { - width: 60, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - fontWeight: 'bold', - backgroundColor: '#F5F5F5', - }, - tableCellRightXSmall: { - width: 30, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellRightSmall: { - width: 40, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableCellRightMedium: { - width: 60, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - tableBorderBottom: { - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - grandTotalRow: { - flexDirection: 'row', - borderTopWidth: 1, - borderTopColor: '#000000', - borderTopStyle: 'solid', - }, - grandTotalLabel: { - flex: 3, - padding: 3, - fontSize: 7, - fontWeight: 'bold', - textAlign: 'right', - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - }, - grandTotalValue: { - flex: 1, - padding: 3, - fontSize: 7, - fontWeight: 'bold', - textAlign: 'right', - borderRightWidth: 0, - }, - allocationSection: { - marginBottom: 8, - }, - allocationTable: { - borderWidth: 1, - borderColor: '#000000', - }, - innerTable: { - marginTop: 5, - borderWidth: 1, - borderColor: '#000000', - }, - innerRow: { - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: '#000000', - borderBottomStyle: 'solid', - }, - innerCell: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - }, - innerCellLast: { - flex: 1, - padding: 3, - fontSize: 7, - }, - innerCellRight: { - flex: 1, - borderRightWidth: 1, - borderRightColor: '#000000', - borderRightStyle: 'solid', - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - innerCellRightLast: { - flex: 1, - padding: 3, - fontSize: 7, - textAlign: 'right', - }, - footer: { - marginTop: 30, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - }, - footerCompany: { - fontSize: 18, - fontWeight: 'bold', - textAlign: 'right', - flex: 1, - color: '#1f74bf', - }, - specialInstructionTable: { - width: '60%', - maxWidth: 300, - borderWidth: 1, - borderColor: '#000000', - flex: 1, - }, -}); - -export default pdfStyles; diff --git a/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx new file mode 100644 index 00000000..f78344d7 --- /dev/null +++ b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ReportExpense } from '@/types/api/report/report-expense'; +import { ColumnDef } from '@tanstack/react-table'; + +type ReportExpenseColumn = + | ColumnDef + | { + header: string; + columns: Array<{ + header: string; + accessorKey?: string; + cell?: (props: { + row: { original: ReportExpense }; + }) => React.ReactNode; + }>; + }; + +const ReportExpenseSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ReportExpenseColumn[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ReportExpenseSkeleton; diff --git a/src/components/pages/report/expense/tab/ReportExpenseTab.tsx b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx new file mode 100644 index 00000000..2581ec5c --- /dev/null +++ b/src/components/pages/report/expense/tab/ReportExpenseTab.tsx @@ -0,0 +1,755 @@ +'use client'; + +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import useSWR from 'swr'; +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import Dropdown from '@/components/dropdown/Dropdown'; +import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import { useFormik } from 'formik'; +import { + ReportExpenseFilterSchema, + type ReportExpenseFilterValues, +} from '@/components/pages/report/expense/filter/ReportExpenseFilter'; +import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; +import RealizationStatusBadge from '@/components/pages/expense/RealizationStatusBadge'; +import Table from '@/components/Table'; +import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { ReportExpense } from '@/types/api/report/report-expense'; +import { ReportExpenseApi } from '@/services/api/report'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useReportTabStore } from '@/stores/report/report-tab.store'; +import Modal, { useModal } from '@/components/Modal'; +import Pagination from '@/components/Pagination'; +import ReportExpenseSkeleton from '@/components/pages/report/expense/skeleton/ReportExpenseSkeleton'; +import { generateReportExpensePDF } from '../export/ReportExpenseExportPDF'; +import { generateReportExpenseExcel } from '../export/ReportExpenseExportXLSX'; +import toast from 'react-hot-toast'; +import { + KandangApi, + LocationApi, + NonstockApi, + SupplierApi, +} from '@/services/api/master-data'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Nonstock } from '@/types/api/master-data/nonstock'; +import { ColumnDef } from '@tanstack/react-table'; +import { httpClient } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; + +interface ReportExpenseTabProps { + tabId: string; +} + +interface FilterParams { + location_id?: string; + supplier_id?: string; + kandang_id?: string; + nonstock_id?: string; + realization_date?: string; + category?: string; + search?: string; +} + +const ReportExpenseTab = ({ tabId }: ReportExpenseTabProps) => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + const [filterParams, setFilterParams] = useState({}); + + // ===== PAGINATION STATE ===== + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const filterModal = useModal(); + + // ===== OPTIONS ===== + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocations, + loadMore: loadMoreLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setSupplierInputValue, + options: supplierOptions, + isLoadingOptions: isLoadingSuppliers, + loadMore: loadMoreSuppliers, + } = useSelect(SupplierApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setKandangInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangs, + loadMore: loadMoreKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name', 'search'); + + const { + setInputValue: setNonstockInputValue, + options: nonstockOptions, + isLoadingOptions: isLoadingNonstocks, + loadMore: loadMoreNonstocks, + } = useSelect(NonstockApi.basePath, 'id', 'name', 'search'); + + const categoryOptions = useMemo( + () => [ + { value: 'BOP', label: 'BOP' }, + { value: 'NON-BOP', label: 'Non BOP' }, + ], + [] + ); + + // ===== FORMIK SETUP ===== + const formik = useFormik({ + initialValues: { + location_id: null, + supplier_id: null, + kandang_id: null, + nonstock_id: null, + realization_date: null, + category: null, + }, + validationSchema: ReportExpenseFilterSchema, + onSubmit: (values) => { + setFilterParams({ + location_id: values.location_id?.value + ? String(values.location_id.value) + : undefined, + supplier_id: values.supplier_id?.value + ? String(values.supplier_id.value) + : undefined, + kandang_id: values.kandang_id?.value + ? String(values.kandang_id.value) + : undefined, + nonstock_id: values.nonstock_id?.value + ? String(values.nonstock_id.value) + : undefined, + realization_date: values.realization_date || undefined, + category: values.category?.value + ? String(values.category.value) + : undefined, + }); + filterModal.closeModal(); + setIsSubmitted(true); + setPage(1); + }, + onReset: () => { + setFilterParams({}); + setIsSubmitted(false); + setPage(1); + }, + }); + + // ===== FILTER VALUES ===== + const locationValue = useMemo( + () => formik.values.location_id, + [formik.values.location_id] + ); + const supplierValue = useMemo( + () => formik.values.supplier_id, + [formik.values.supplier_id] + ); + const kandangValue = useMemo( + () => formik.values.kandang_id, + [formik.values.kandang_id] + ); + const nonstockValue = useMemo( + () => formik.values.nonstock_id, + [formik.values.nonstock_id] + ); + const categoryValue = useMemo( + () => formik.values.category, + [formik.values.category] + ); + + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + if (filterParams.location_id) count += 1; + if (filterParams.supplier_id) count += 1; + if (filterParams.kandang_id) count += 1; + if (filterParams.nonstock_id) count += 1; + if (filterParams.realization_date) count += 1; + if (filterParams.category) count += 1; + return count; + }, [filterParams]); + + const hasFilters = activeFiltersCount > 0; + + // ===== DATA FETCHING ===== + const { data: reportExpenseResponse, isLoading } = useSWR( + isSubmitted + ? () => { + const params = new URLSearchParams(); + if (filterParams.location_id) + params.append('location_id', filterParams.location_id); + if (filterParams.supplier_id) + params.append('supplier_id', filterParams.supplier_id); + if (filterParams.kandang_id) + params.append('kandang_id', filterParams.kandang_id); + if (filterParams.nonstock_id) + params.append('nonstock_id', filterParams.nonstock_id); + if (filterParams.realization_date) + params.append('realization_date', filterParams.realization_date); + if (filterParams.category) + params.append('category', filterParams.category); + params.append('page', String(page)); + params.append('limit', String(pageSize)); + + return [`${ReportExpenseApi.basePath}?${params.toString()}`]; + } + : null, + ([url]: string[]) => httpClient>(url) + ); + + const data: ReportExpense[] = useMemo( + () => + isResponseSuccess(reportExpenseResponse) + ? (reportExpenseResponse.data as ReportExpense[]) || [] + : [], + [reportExpenseResponse] + ); + + const meta = useMemo( + () => + isResponseSuccess(reportExpenseResponse) && reportExpenseResponse.meta + ? reportExpenseResponse.meta + : null, + [reportExpenseResponse] + ); + + // ===== EXPORT DATA FETCHER ===== + const reportExpenseExport = useCallback(async (): Promise< + ReportExpense[] | null + > => { + const params = new URLSearchParams(); + if (filterParams.location_id) + params.append('location_id', filterParams.location_id); + if (filterParams.supplier_id) + params.append('supplier_id', filterParams.supplier_id); + if (filterParams.kandang_id) + params.append('kandang_id', filterParams.kandang_id); + if (filterParams.nonstock_id) + params.append('nonstock_id', filterParams.nonstock_id); + if (filterParams.realization_date) + params.append('realization_date', filterParams.realization_date); + if (filterParams.category) params.append('category', filterParams.category); + params.append('limit', '100'); + params.append('page', '1'); + + const response = await httpClient>( + `${ReportExpenseApi.basePath}?${params.toString()}` + ); + + return isResponseSuccess(response) ? response.data : null; + }, [filterParams]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await reportExpenseExport(); + + if (!allDataForExport || allDataForExport.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + await generateReportExpenseExcel(allDataForExport); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [reportExpenseExport]); + + const handleExportPDF = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allData = await reportExpenseExport(); + if (!allData || allData.length === 0) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const pdfParams = { + location_name: locationValue?.label, + supplier_name: supplierValue?.label, + realization_date: formik.values.realization_date || undefined, + }; + + await generateReportExpensePDF(allData, pdfParams); + + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [ + reportExpenseExport, + locationValue, + supplierValue, + kandangValue, + nonstockValue, + categoryValue, + formik.values.realization_date, + ]); + + // ===== REGISTER TAB ACTIONS TO STORE ===== + const setTabActions = useReportTabStore((state) => state.setTabActions); + const clearTabActions = useReportTabStore((state) => state.clearTabActions); + + useEffect(() => { + setTabActions( + tabId, +
+ + + +
+ + + Export + +
+ + +
+ + } + > + + + +
+ ); + }, [ + tabId, + hasFilters, + activeFiltersCount, + isAnyExportLoading, + handleExportExcel, + handleExportPDF, + setTabActions, + ]); + + useEffect(() => { + return () => { + clearTabActions(tabId); + }; + }, [tabId, clearTabActions]); + + // ===== TABLE COLUMNS DEFINITION ===== + const columns = useMemo((): ColumnDef[] => { + return [ + { + header: 'No', + cell: (props) => (page - 1) * pageSize + props.row.index + 1, + }, + { + header: 'No. PO', + accessorKey: 'po_number', + }, + { + header: 'No. Referensi', + accessorKey: 'reference_number', + }, + { + header: 'Tanggal Realisasi', + accessorKey: 'realization_date', + cell: ({ row }) => { + return formatDate(row.original?.realization_date, 'DD MMM, YYYY'); + }, + }, + { + header: 'Tanggal Transaksi', + accessorKey: 'transaction_date', + cell: ({ row }) => { + return formatDate(row.original?.transaction_date, 'DD MMM, YYYY'); + }, + }, + { + header: 'Kategori', + accessorKey: 'category', + }, + { + header: 'Produk', + accessorFn: (row) => row.pengajuan?.nonstock?.name, + }, + { + header: 'Supplier', + accessorFn: (row) => row.supplier?.name, + }, + { + header: 'Lokasi', + accessorFn: (row) => row.kandang?.location?.name, + }, + { + header: 'Kandang', + accessorFn: (row) => row.kandang?.name, + }, + { + header: 'Pengajuan', + columns: [ + { + header: 'Qty', + id: 'qty_pengajuan', + accessorFn: (row) => row.pengajuan?.qty, + cell: ({ row }) => + row.original.pengajuan?.qty?.toLocaleString('id-ID') || '0', + }, + { + header: 'Harga', + id: 'harga_pengajuan', + accessorFn: (row) => row.pengajuan?.price, + cell: ({ row }) => + formatCurrency(row.original.pengajuan?.price || 0), + }, + { + header: 'Total', + id: 'total_pengajuan', + accessorFn: (row) => + (row.pengajuan?.qty || 0) * (row.pengajuan?.price || 0), + cell: ({ row }) => { + const total = + (row.original.pengajuan?.qty || 0) * + (row.original.pengajuan?.price || 0); + return formatCurrency(total); + }, + }, + ], + }, + { + header: 'Realisasi', + columns: [ + { + header: 'Qty', + id: 'qty_realisasi', + accessorFn: (row) => row.realisasi?.qty, + cell: ({ row }) => + row.original.realisasi?.qty?.toLocaleString('id-ID') || '0', + }, + { + header: 'Harga', + id: 'harga_realisasi', + accessorFn: (row) => row.realisasi?.price, + cell: ({ row }) => + formatCurrency(row.original.realisasi?.price || 0), + }, + { + header: 'Total', + id: 'total_realisasi', + accessorFn: (row) => + (row.realisasi?.qty || 0) * (row.realisasi?.price || 0), + cell: ({ row }) => { + const total = + (row.original.realisasi?.qty || 0) * + (row.original.realisasi?.price || 0); + return formatCurrency(total); + }, + }, + ], + }, + { + header: 'Status Pencairan', + cell: (props) => ( + + ), + }, + { + header: 'Status BOP', + cell: (props) => ( + + ), + }, + ]; + }, [page, pageSize]); + + return ( + <> +
+ {!isSubmitted ? ( + + } + title='No Filters Selected' + subtitle='Please choose filters to narrow down your results and make your search easier.' + /> + ) : isLoading ? ( +
+ +
+ ) : !data || data.length === 0 ? ( + + } + title='Data Not Yet Available' + subtitle='Please change your filters to get the data.' + /> + ) : ( + <> +
+ {meta && ( +
+ + setPage((currPage) => + currPage > 1 ? currPage - 1 : currPage + ) + } + onNextPage={() => + setPage((currPage) => + meta && meta.total_pages && currPage < meta.total_pages + ? currPage + 1 + : currPage + ) + } + onPageChange={(pageNumber) => setPage(pageNumber)} + rowOptions={[10, 20, 50, 100]} + onRowChange={setPageSize} + /> +
+ )} + + )} + + + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+ +
+ {/* Modal Body */} +
+ { + formik.setFieldValue('location_id', val); + formik.setFieldValue('kandang_id', null); + }} + onInputChange={setLocationInputValue} + onMenuScrollToBottom={loadMoreLocations} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('kandang_id', val); + }} + onInputChange={setKandangInputValue} + onMenuScrollToBottom={loadMoreKandangs} + isClearable + isDisabled={!formik.values.location_id} + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('supplier_id', val); + }} + onInputChange={setSupplierInputValue} + onMenuScrollToBottom={loadMoreSuppliers} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('nonstock_id', val); + }} + onInputChange={setNonstockInputValue} + onMenuScrollToBottom={loadMoreNonstocks} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue('category', val); + }} + isClearable + className={{ wrapper: 'w-full' }} + /> + + { + formik.setFieldValue( + 'realization_date', + e.target.value || null + ); + }} + className={{ wrapper: 'w-full' }} + /> +
+ + {/* Modal Footer */} +
+ + +
+ +
+ + ); +}; + +export default ReportExpenseTab; From e9da5210addddddab4be15c085775b10fbe5dd7f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 10:22:13 +0700 Subject: [PATCH 094/149] refactor(FE): Simplify type definition for ReportExpenseColumn --- .../pages/report/expense/skeleton/ReportExpenseSkeleton.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx index f78344d7..3e13c539 100644 --- a/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx +++ b/src/components/pages/report/expense/skeleton/ReportExpenseSkeleton.tsx @@ -11,9 +11,7 @@ type ReportExpenseColumn = columns: Array<{ header: string; accessorKey?: string; - cell?: (props: { - row: { original: ReportExpense }; - }) => React.ReactNode; + cell?: (props: { row: { original: ReportExpense } }) => React.ReactNode; }>; }; From ec3a0367ddfef1762816ba6d97493c6ab875eb59 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Feb 2026 10:37:12 +0700 Subject: [PATCH 095/149] refactor(FE): Hide delete button if only one item remains in table --- .../table-view/SalesOrderProductTable.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 18f6145b..70282648 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -49,17 +49,19 @@ const SalesOrderProductTable = ({ > - + {data.length > 1 && ( + + )} )} From e8e4f7b877224d0f877f92075200d5f4b3565659 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 15 Feb 2026 08:47:07 +0700 Subject: [PATCH 096/149] refactor(FE): Simplify product filtering logic in RecordingForm --- .../production/recording/form/RecordingForm.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 7ea58f0f..b903a8af 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -484,6 +484,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { location_id: depletionProductsLocationId, kandang_id: depletionProductsKandangId, + type: 'AYAM', }); const today = new Date().toISOString().split('T')[0]; @@ -784,18 +785,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (isResponseSuccess(depletionProductsData) && selectedKandang) { const data = depletionProductsData.data as unknown as ProductWarehouse[]; data.forEach((product) => { - const productName = product.product.name; - - if ( - productName.toLowerCase().includes('culling') || - productName.toLowerCase().includes('mati') || - productName.toLowerCase().includes('afkir') - ) { - options.push({ - value: product.id, - label: product.product.name, - }); - } + options.push({ + value: product.id, + label: product.product.name, + }); }); } From 512ccddfc78c0b7b1fad150d20bd8ca9f27d1670 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 09:59:50 +0700 Subject: [PATCH 097/149] refactor(FE): Refactor ChickinForm and ProjectFlockClosingForm components --- .../production/chickin/form/ChickinForm.tsx | 206 ++++++++--------- .../closing/ProjectFlockClosingForm.tsx | 216 ++++++++++-------- 2 files changed, 214 insertions(+), 208 deletions(-) diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index bd3ff57c..c28b456d 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -16,6 +16,7 @@ import ChickinLogsView from '@/components/pages/production/chickin/form/tabs/Chi import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { Icon } from '@iconify/react'; import Badge from '@/components/Badge'; +import StatusBadge from '@/components/helper/StatusBadge'; import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line'; import RequirePermission from '@/components/helper/RequirePermission'; import { BaseApproval } from '@/types/api/api-general'; @@ -53,135 +54,126 @@ const ChickinFormKandang = ({ }; return ( -
+
+ {/* Header */} - {/* Informasi Kandang */} -
-
-

Informasi Kandang

+ {approvals && !approvalsLoading && ( + + )} - {approvals && !approvalsLoading && ( -
- -
- )} + {/* Informasi Kandang */} +
+

+ Informasi Kandang +

{/* Badge Row */}
- - {' '} - Aktif - + text='Aktif' + className={{ badge: 'w-fit text-nowrap' }} + />
- - - {` Kapasitas ${formatNumber(initialValues.kandang.capacity)} Ekor`} - + text={` Kapasitas ${formatNumber(initialValues.kandang.capacity)} Ekor`} + className={{ badge: 'w-fit text-nowrap' }} + />
- {/* Information Grid */} -
- {/* Area */} -
- Area + {/* Information Card */} + +
+
+
+ {' '} + Area +
+
+ {initialValues.project_flock.area.name} +
+
+
+
+ {' '} + Lokasi +
+
+ {initialValues.project_flock?.location.name} +
+
+
+
+ {' '} + Kandang +
+
+ {initialValues.kandang.name} +
+
+
+
+ {' '} + Jumlah DOC +
+
+ {formatNumber( + initialValues.chickins?.reduce( + (total, chickin) => total + chickin.usage_qty, + 0 + ) ?? 0 + )}{' '} + Ekor +
+
-
- {initialValues.project_flock.area.name} -
- - {/* Lokasi */} -
- Lokasi -
-
- {initialValues.project_flock?.location.name} -
- - {/* Kandang */} -
- Kandang -
-
{initialValues.kandang.name}
- - {/* Jumlah DOC */} -
- Jumlah DOC -
-
- {formatNumber( - initialValues.chickins?.reduce( - (total, chickin) => total + chickin.usage_qty, - 0 - ) ?? 0 - )}{' '} - Ekor -
-
+
-
-
-

Informasi Chick In

+ {/* Informasi Chick In */} +
+

+ Informasi Chick In +

{/* Badge Row */}
- - {' '} - Perlu Chick In ({initialValues.available_qtys?.length ?? 0}) - +
- setOpenChickin(!openChickin)} - > - {`Riwayat Chick In ${formatNumber(initialValues.chickins?.length ?? 0)}`} - - + text={ + <> + {`Riwayat Chick In ${formatNumber(initialValues.chickins?.length ?? 0)}`} + + + } + className={{ badge: 'w-fit text-nowrap cursor-pointer' }} + />
{openChickin && ( @@ -198,7 +190,7 @@ const ChickinFormKandang = ({ afterSubmit={afterSubmitFormChickin} /> -
+ ); }; diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index aab21172..162eb6a2 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -1,10 +1,12 @@ 'use client'; import Button from '@/components/Button'; +import Card from '@/components/Card'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import Table from '@/components/Table'; import Badge from '@/components/Badge'; -import { cn, formatDate, formatNumber, formatTitleCase } from '@/lib/helper'; +import StatusBadge from '@/components/helper/StatusBadge'; +import { formatDate, formatNumber, formatTitleCase } from '@/lib/helper'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ClosingExpense, @@ -20,7 +22,6 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { useRouter } from 'next/navigation'; -import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ApprovalApi } from '@/services/api/approval'; import RequirePermission from '@/components/helper/RequirePermission'; @@ -78,112 +79,113 @@ const ProjectFlockClosingForm = ({ closeModal.closeModal(); }; - const errorStock = useMemo(() => { - return isResponseSuccess(closingData) - ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0) - : true; - }, [closingData]); + // const errorStock = useMemo(() => { + // return isResponseSuccess(closingData) + // ? closingData?.data?.stock_remaining.every((stock) => stock.quantity > 0) + // : true; + // }, [closingData]); - const errorExpense = useMemo(() => { - return isResponseSuccess(closingData) - ? closingData?.data?.expenses.every((expense) => expense.step < 5) - : true; - }, [closingData]); + // const errorExpense = useMemo(() => { + // return isResponseSuccess(closingData) + // ? closingData?.data?.expenses.every((expense) => expense.step < 5) + // : true; + // }, [closingData]); const isCanCloseValid = true; return ( <> -
+
+ {/* Header */} + leftIconClassName='hover:text-gray-400' + subtitle='Close Flock' + className='sticky top-0 z-10 bg-base-100' + /> {/* Informasi Kandang */} -
-
-

Informasi Kandang

+
+

+ Informasi Kandang +

{/* Badge Row */}
- - {' '} - Aktif - + text='Aktif' + className={{ badge: 'w-fit text-nowrap' }} + />
- - - {` Kapasitas ${formatNumber(projectFlockKandang.kandang?.capacity)} Ekor`} - + text={` Kapasitas ${formatNumber(projectFlockKandang.kandang?.capacity)} Ekor`} + className={{ badge: 'w-fit text-nowrap' }} + />
- {/* Information Grid */} -
- {/* Area */} -
- Area + {/* Information Card */} + +
+
+
+ {' '} + Area +
+
+ {projectFlock.area?.name} +
+
+
+
+ {' '} + Lokasi +
+
+ {projectFlock.location?.name} +
+
+
+
+ {' '} + Kandang +
+
+ {projectFlockKandang.kandang?.name} +
+
+
+
+ {' '} + Jumlah DOC +
+
+ {formatNumber( + projectFlockKandang.chickins?.reduce( + (total, chickin) => total + chickin.usage_qty, + 0 + ) ?? 0 + )}{' '} + Ekor +
+
-
{projectFlock.area?.name}
- - {/* Lokasi */} -
- Lokasi -
-
{projectFlock.location?.name}
- - {/* Kandang */} -
- Kandang -
-
- {projectFlockKandang.kandang?.name} -
- - {/* Jumlah DOC */} -
- Jumlah - DOC -
-
- {formatNumber( - projectFlockKandang.chickins?.reduce( - (total, chickin) => total + chickin.usage_qty, - 0 - ) ?? 0 - )}{' '} - Ekor -
-
+
{/* Table Biaya */} -
-
-

Biaya

+
+

+ Biaya +

data={ isResponseSuccess(closingData) ? closingData.data?.expenses : [] @@ -192,6 +194,16 @@ const ProjectFlockClosingForm = ({ { header: 'PO Number', accessorKey: 'po_number', + cell(props) { + return props.row.original.po_number || '-'; + }, + }, + { + header: 'Ref Number', + accessorKey: 'reference_number', + cell(props) { + return props.row.original.reference_number || '-'; + }, }, { header: 'Total', @@ -208,11 +220,11 @@ const ProjectFlockClosingForm = ({ }} variant='soft' color={ - props.row.original.step < 5 - ? props.row.original.step == 1 + props.row.original.step === 6 + ? 'success' + : props.row.original.step === 1 ? 'neutral' - : 'success' - : 'error' + : 'warning' } > {formatTitleCase(props.row.original.step_name)} @@ -222,13 +234,13 @@ const ProjectFlockClosingForm = ({ }, ]} className={{ - containerClassName: cn('my-4'), + containerClassName: 'mb-0', tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', tableClassName: 'font-inter w-full table-sm min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', + headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', + bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', paginationClassName: 'hidden', @@ -242,9 +254,10 @@ const ProjectFlockClosingForm = ({
{/* Table Persediaan Gudang */} -
-
-

Persediaan Gudang

+
+

+ Persediaan Gudang +

data={ isResponseSuccess(closingData) @@ -270,13 +283,13 @@ const ProjectFlockClosingForm = ({ }, ]} className={{ - containerClassName: cn('my-4'), + containerClassName: 'mb-0', tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', tableClassName: 'font-inter w-full table-sm min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', + headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', + bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', paginationClassName: 'hidden', @@ -289,10 +302,11 @@ const ProjectFlockClosingForm = ({ )} */}
-
+
+ ); }; From 9d6cc901629052e507e33273ca5b4fb7a7a4361c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 11:52:30 +0700 Subject: [PATCH 098/149] refactor(FE): Update table columns and improve UI for Project Flock pages --- .../closing/ProjectFlockClosingForm.tsx | 20 +++++--- .../detail/ProjectFlockDetail.tsx | 48 ++++++++++++++----- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 162eb6a2..da54e4d0 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -191,13 +191,6 @@ const ProjectFlockClosingForm = ({ isResponseSuccess(closingData) ? closingData.data?.expenses : [] } columns={[ - { - header: 'PO Number', - accessorKey: 'po_number', - cell(props) { - return props.row.original.po_number || '-'; - }, - }, { header: 'Ref Number', accessorKey: 'reference_number', @@ -205,9 +198,19 @@ const ProjectFlockClosingForm = ({ return props.row.original.reference_number || '-'; }, }, + { + header: 'PO Number', + accessorKey: 'po_number', + cell(props) { + return props.row.original.po_number || '-'; + }, + }, { header: 'Total', accessorKey: 'total', + cell(props) { + return formatNumber(props.row.original.total); + }, }, { header: 'Status', @@ -276,6 +279,9 @@ const ProjectFlockClosingForm = ({ { header: 'Quantity', accessorKey: 'quantity', + cell(props) { + return formatNumber(props.row.original.quantity); + }, }, { header: 'UOM', diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 47491dfa..c2b8e903 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -226,15 +226,37 @@ const ProjectFlockDetail = ({

- Kandang Aktif + Kandang

-
- +
+ {projectFlock.kandangs?.filter( + (kandang) => kandang.status !== 'NON_ACTIVE' + ).length > 0 && ( + kandang.status !== 'NON_ACTIVE' + ).length ?? 0 + })`} + className={{ badge: 'w-fit' }} + /> + )} + + {projectFlock.kandangs?.filter( + (kandang) => kandang.status === 'NON_ACTIVE' + ).length > 0 && ( + kandang.status === 'NON_ACTIVE' + ).length ?? 0 + })`} + className={{ badge: 'w-fit' }} + /> + )} } - className={{ badge: 'w-fit text-nowrap cursor-pointer' }} + className={{ badge: 'w-fit cursor-pointer' }} />
@@ -368,10 +390,12 @@ const ProjectFlockDetail = ({ - +
+ +
From da27f4c5816f9ccf27b58b22ae6ef79ac1647984 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 13:15:06 +0700 Subject: [PATCH 099/149] refactor(FE): Refactor status badge logic in ProjectFlockClosingForm --- .../closing/ProjectFlockClosingForm.tsx | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index da54e4d0..860a426a 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -24,6 +24,26 @@ import toast from 'react-hot-toast'; import { useRouter } from 'next/navigation'; import { ApprovalApi } from '@/services/api/approval'; import RequirePermission from '@/components/helper/RequirePermission'; +import { Color } from '@/types/theme'; + +const getExpenseStatusBadgeColor = (step: number): Color => { + switch (step) { + case 1: + return 'neutral'; + case 2: + return 'info'; + case 3: + return 'warning'; + case 4: + return 'error'; + case 5: + return 'warning'; + case 6: + return 'success'; + default: + return 'neutral'; + } +}; const ProjectFlockClosingForm = ({ projectFlock, @@ -217,21 +237,15 @@ const ProjectFlockClosingForm = ({ accessorKey: 'status', cell(props) { return ( - - {formatTitleCase(props.row.original.step_name)} - + /> ); }, }, From d4c6a05c0cbbcd1cbbdda639e7be80820e896211 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 13:21:29 +0700 Subject: [PATCH 100/149] refactor(FE): Update button behavior based on kandang status --- .../detail/ProjectFlockDetail.tsx | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index c2b8e903..1ad3f983 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -398,24 +398,26 @@ const ProjectFlockDetail = ({
- - - - - + + + + )} - Close + {selectedKandang?.status === 'NON_ACTIVE' ? ( + <> + Unclose + + ) : ( + <> + Close + + )} From ed576fc8eb4d3549db47be462d08710341c6f744 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 13:49:02 +0700 Subject: [PATCH 101/149] feat(FE): Improve empty state handling and add "Unclose Flock" functionality --- src/components/Table.tsx | 22 +++++++++++---- .../closing/ProjectFlockClosingForm.tsx | 28 +++++++++++++------ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b40d9db5..d9d81543 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -85,8 +85,8 @@ const DUMMY_SKELETON_DATA = Array.from({ length: 10 }, (_, index) => ({ })); const emptyContentDefaultValue = ( -
- +
+ Tidak ada data yang dapat ditampilkan...
@@ -452,6 +452,20 @@ const Table = ({ ); })} + + {(data.length === 0 || table.getRowModel().rows.length === 0) && + !isLoading && ( +
+ + + )} ({
+ {emptyContent} +
- {(data.length === 0 || table.getRowModel().rows.length === 0) && - !isLoading && - emptyContent} - {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 860a426a..996425a0 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -70,6 +70,10 @@ const ProjectFlockClosingForm = ({ ) ); + const isKandangClosed = useMemo(() => { + return projectFlockKandang.kandang?.status === 'NON_ACTIVE'; + }, [projectFlockKandang]); + const isCanClose = useMemo(() => { return isResponseSuccess(projectFlockKandangApprovals) ? projectFlockKandangApprovals?.data?.[0]?.step_number <= 2 @@ -81,8 +85,10 @@ const ProjectFlockClosingForm = ({ const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( projectFlockKandang?.id as number, { - closed_date: isCanClose ? formatDate(new Date(), 'YYYY-MM-DD') : '', - action: isCanClose ? 'close' : 'unclose', + closed_date: !isKandangClosed + ? formatDate(new Date(), 'YYYY-MM-DD') + : '', + action: !isKandangClosed ? 'close' : 'unclose', } ); @@ -121,7 +127,7 @@ const ProjectFlockClosingForm = ({ leftIcon='heroicons:chevron-left' leftIconHref={`/production/project-flock/detail?projectFlockId=${projectFlock.id}`} leftIconClassName='hover:text-gray-400' - subtitle='Close Flock' + subtitle={isKandangClosed ? 'Unclose Flock' : 'Close Flock'} className='sticky top-0 z-10 bg-base-100' /> @@ -134,8 +140,8 @@ const ProjectFlockClosingForm = ({ {/* Badge Row */}
@@ -332,8 +338,14 @@ const ProjectFlockClosingForm = ({ disabled={!isCanCloseValid} onClick={() => closeModal.openModal()} > - {' '} - {isCanClose ? 'Close' : 'Unclose'} + {' '} + {isKandangClosed ? 'Unclose' : 'Close'}
@@ -342,7 +354,7 @@ const ProjectFlockClosingForm = ({ ref={closeModal.ref} type='error' text={ - isCanClose + !isKandangClosed ? 'Apakah kamu yakin ingin mengakhiri project ini ? *Pastikan persediaan produk di gudang terkait sudah kosong, dan BOP sudah selesai' : 'Apakah kamu yakin ingin membuka kembali project ini ? *Project ini akan kembali ke status aktif' } From e94967ea4c3c35938dd742c062941f69a94e75d6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 14:28:55 +0700 Subject: [PATCH 102/149] refactor(FE): Adjust grid column layout based on selectedKandang status --- .../project-flock/detail/ProjectFlockDetail.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 1ad3f983..44e5cf98 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -397,7 +397,13 @@ const ProjectFlockDetail = ({ />
-
+
{selectedKandang?.status !== 'NON_ACTIVE' && ( Date: Wed, 18 Feb 2026 14:33:28 +0700 Subject: [PATCH 103/149] refactor(FE): Replace Badge with StatusBadge in ProjectFlockDetail --- .../project-flock/detail/ProjectFlockDetail.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 44e5cf98..d817c69b 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -377,13 +377,13 @@ const ProjectFlockDetail = ({ disabled={projectFlock?.approval?.step_number == 1} />
- - Kapasitas {kandang?.capacity} Ekor - + Kapasitas {kandang?.capacity} Ekor} + className={{ badge: 'w-fit text-nowrap' }} + />
))} From 15289951e6f6dc9b18d106793e29d902d2a0d9d7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 14:42:41 +0700 Subject: [PATCH 104/149] refactor(FE): Remove unused CSS classes from table components --- .../project-flock/closing/ProjectFlockClosingForm.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 996425a0..7ffe7b24 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -258,15 +258,14 @@ const ProjectFlockClosingForm = ({ ]} className={{ containerClassName: 'mb-0', - tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', - tableClassName: 'font-inter w-full table-sm min-h-full!', + tableWrapperClassName: 'overflow-x-auto max-w-120', + tableClassName: 'font-inter w-full table-sm', headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', - paginationClassName: 'hidden', }} /> {/* {errorExpense && ( @@ -310,15 +309,14 @@ const ProjectFlockClosingForm = ({ ]} className={{ containerClassName: 'mb-0', - tableWrapperClassName: 'overflow-x-auto min-h-full! max-w-120', - tableClassName: 'font-inter w-full table-sm min-h-full!', + tableWrapperClassName: 'overflow-x-auto max-w-120', + tableClassName: 'font-inter w-full table-sm', headerRowClassName: 'border-b border-base-content/10', headerColumnClassName: 'px-3 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', bodyRowClassName: 'border-b border-base-content/10', bodyColumnClassName: 'px-3 py-3 last:flex last:flex-row last:justify-end', - paginationClassName: 'hidden', }} /> {/* {errorStock && ( From 02165df89ccf6c8b5671d6bf88371487cad13f0a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 14:50:11 +0700 Subject: [PATCH 105/149] refactor(FE): Update status badge text to use English labels --- .../project-flock/closing/ProjectFlockClosingForm.tsx | 2 +- .../production/project-flock/detail/ProjectFlockDetail.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index 7ffe7b24..f963a793 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -141,7 +141,7 @@ const ProjectFlockClosingForm = ({
diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index d817c69b..db3d45aa 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -235,7 +235,7 @@ const ProjectFlockDetail = ({ ).length > 0 && ( kandang.status !== 'NON_ACTIVE' ).length ?? 0 @@ -249,7 +249,7 @@ const ProjectFlockDetail = ({ ).length > 0 && ( kandang.status === 'NON_ACTIVE' ).length ?? 0 From 2169c0ea6298beaae88dae93be4f2589ef4b3bb0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 15:00:26 +0700 Subject: [PATCH 106/149] refactor(FE): Update StatusBadge color and text in ChickinForm --- src/components/pages/production/chickin/form/ChickinForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index c28b456d..b9c73934 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -77,8 +77,8 @@ const ChickinFormKandang = ({ {/* Badge Row */}
From d085b18788e2b745d6062916335bb3ed8aa36ff8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 15:40:06 +0700 Subject: [PATCH 107/149] refactor(FE): Refactor folder structure for closing-related components --- src/components/pages/closing/ClosingDetail.tsx | 12 ++++++------ src/components/pages/closing/ClosingsTable.tsx | 2 +- .../closing/{ => finance}/ClosingFinanceTable.tsx | 0 .../closing/{ => overhead}/ClosingOverheadTable.tsx | 0 .../ClosingSapronakCalculationTable.tsx | 0 .../ClosingIncomingSapronaksSummaryTable.tsx | 0 .../{ => sapronak}/ClosingIncomingSapronaksTable.tsx | 0 .../ClosingOutgoingSapronaksSummaryTable.tsx | 0 .../{ => sapronak}/ClosingOutgoingSapronaksTable.tsx | 0 .../closing/{ => tab}/ClosingFinanceTabContent.tsx | 2 +- .../closing/{ => tab}/ClosingOverheadTabContent.tsx | 2 +- .../{ => tab}/ClosingProductionDataTabContent.tsx | 0 .../ClosingSapronakCalculationTabContent.tsx | 2 +- .../closing/{ => tab}/ClosingSapronakTabContent.tsx | 8 ++++---- 14 files changed, 14 insertions(+), 14 deletions(-) rename src/components/pages/closing/{ => finance}/ClosingFinanceTable.tsx (100%) rename src/components/pages/closing/{ => overhead}/ClosingOverheadTable.tsx (100%) rename src/components/pages/closing/{ => sapronak-calculation}/ClosingSapronakCalculationTable.tsx (100%) rename src/components/pages/closing/{ => sapronak}/ClosingIncomingSapronaksSummaryTable.tsx (100%) rename src/components/pages/closing/{ => sapronak}/ClosingIncomingSapronaksTable.tsx (100%) rename src/components/pages/closing/{ => sapronak}/ClosingOutgoingSapronaksSummaryTable.tsx (100%) rename src/components/pages/closing/{ => sapronak}/ClosingOutgoingSapronaksTable.tsx (100%) rename src/components/pages/closing/{ => tab}/ClosingFinanceTabContent.tsx (77%) rename src/components/pages/closing/{ => tab}/ClosingOverheadTabContent.tsx (89%) rename src/components/pages/closing/{ => tab}/ClosingProductionDataTabContent.tsx (100%) rename src/components/pages/closing/{ => tab}/ClosingSapronakCalculationTabContent.tsx (92%) rename src/components/pages/closing/{ => tab}/ClosingSapronakTabContent.tsx (77%) diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index c3c91a5a..51f7618d 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -6,17 +6,17 @@ import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Tabs from '@/components/Tabs'; import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; -import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent'; -import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent'; +import ClosingSapronakTabContent from '@/components/pages/closing/tab/ClosingSapronakTabContent'; +import ClosingProductionDataTabContent from '@/components/pages/closing/tab/ClosingProductionDataTabContent'; import { ClosingGeneralInformation, BaseClosingSales, ClosingHppExpedition, } from '@/types/api/closing'; -import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; -import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent'; -import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent'; +import ClosingSapronakCalculationTabContent from '@/components/pages/closing/tab/ClosingSapronakCalculationTabContent'; +import ClosingOverheadTabContent from '@/components/pages/closing/tab/ClosingOverheadTabContent'; +import ClosingFinanceTabContent from '@/components/pages/closing/tab/ClosingFinanceTabContent'; import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable'; import ClosingKandangList from '@/components/pages/closing/ClosingKandangList'; @@ -96,7 +96,7 @@ const ClosingDetail: React.FC = ({ return ( <> -
+
diff --git a/src/components/pages/closing/ClosingGeneralInformationTable.tsx b/src/components/pages/closing/table/ClosingGeneralInformationTable.tsx similarity index 100% rename from src/components/pages/closing/ClosingGeneralInformationTable.tsx rename to src/components/pages/closing/table/ClosingGeneralInformationTable.tsx diff --git a/src/stores/closing/closing-tab.store.ts b/src/stores/closing/closing-tab.store.ts new file mode 100644 index 00000000..1f81c26a --- /dev/null +++ b/src/stores/closing/closing-tab.store.ts @@ -0,0 +1,21 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { + createClosingTabSlice, + ClosingTabSlice, +} from '@/stores/closing/slices/closing-tab.slice'; + +export type ClosingTabStore = ClosingTabSlice; + +export const useClosingTabStore = create()( + devtools( + (...args) => ({ + ...createClosingTabSlice(...args), + }), + { + name: 'ClosingTabStore', + } + ) +); diff --git a/src/stores/closing/slices/closing-tab.slice.ts b/src/stores/closing/slices/closing-tab.slice.ts new file mode 100644 index 00000000..cd47bbdc --- /dev/null +++ b/src/stores/closing/slices/closing-tab.slice.ts @@ -0,0 +1,37 @@ +import { ReactNode } from 'react'; +import { StateCreator } from 'zustand'; + +export type ClosingTabSlice = { + // State - actions per tab ID + tabActions: Record; + + // Actions + setTabActions: (tabId: string, actions: ReactNode) => void; + clearTabActions: (tabId: string) => void; + clearAllTabActions: () => void; +}; + +export const createClosingTabSlice: StateCreator< + ClosingTabSlice, + [], + [], + ClosingTabSlice +> = (set) => ({ + tabActions: {}, + + setTabActions: (tabId, actions) => + set((state) => ({ + tabActions: { + ...state.tabActions, + [tabId]: actions, + }, + })), + + clearTabActions: (tabId) => + set((state) => { + const { [tabId]: _, ...rest } = state.tabActions; + return { tabActions: rest }; + }), + + clearAllTabActions: () => set({ tabActions: {} }), +}); From 32354e3c2df7cb1ce66b4573675bf9d22c19dac0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 18 Feb 2026 16:32:26 +0700 Subject: [PATCH 111/149] refactor(FE): Adjust padding on tab header wrapper in ClosingDetailTabs --- src/components/pages/closing/ClosingDetailTabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index 2ec3fe1e..2a5fa638 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -136,7 +136,7 @@ const ClosingDetail: React.FC = ({ variant='boxed' className={{ tabHeaderWrapper: - 'justify-between items-center p-3 border-b border-base-content/10', + 'justify-between items-center py-3 border-b border-base-content/10', tab: 'w-fit', content: 'p-0 m-0', }} From 0235494d46c28ca408061243a564d34d1b13d479 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:29:01 +0700 Subject: [PATCH 112/149] refactor(FE): Refactor HPP Expedition handling in ClosingDetailPage --- src/app/closing/detail/page.tsx | 21 --------- .../pages/closing/ClosingDetailTabs.tsx | 9 +--- .../closing/tab/HppExpeditionClosingTab.tsx | 19 ++++++++ .../table/HppExpeditionClosingTable.tsx | 43 ++++++++++++++----- 4 files changed, 54 insertions(+), 38 deletions(-) create mode 100644 src/components/pages/closing/tab/HppExpeditionClosingTab.tsx diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 8f164f44..d081951c 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -46,21 +46,6 @@ const ClosingDetailPage = () => { : ClosingApi.getPenjualan(Number(closingId)) ); - const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR( - kandangId - ? `hpp-ekspedisi-${closingId}-${kandangId}` - : closingId - ? `hpp-ekspedisi-${closingId}` - : null, - () => - kandangId - ? ClosingApi.getHppEkspedisiByKandang( - Number(closingId), - Number(kandangId) - ) - : ClosingApi.getHppEkspedisi(Number(closingId)) - ); - if (!closingId) { router.back(); @@ -79,7 +64,6 @@ const ClosingDetailPage = () => { const isLoading = isLoadingClosing || isLoadingSales || - isLoadingHppEkspedisi || isLoadingProject || isLoadingKandang; @@ -92,11 +76,6 @@ const ClosingDetailPage = () => { id={Number(closingId)} initialValue={closing.data} salesData={isResponseSuccess(salesData) ? salesData.data : undefined} - hppExpeditionData={ - isResponseSuccess(hppEkspedisiData) - ? hppEkspedisiData.data - : undefined - } projectData={ isResponseSuccess(projectData) ? projectData.data : undefined } diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index 2a5fa638..94340283 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -12,13 +12,12 @@ import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionD import { ClosingGeneralInformation, BaseClosingSales, - ClosingHppExpedition, } from '@/types/api/closing'; import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab'; import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab'; import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab'; import SalesClosingTable from '@/components/pages/closing/table/SalesClosingTable'; -import HppExpeditionClosingTable from './table/HppExpeditionClosingTable'; +import HppExpeditionClosingTab from '@/components/pages/closing/tab/HppExpeditionClosingTab'; import ClosingKandangList from '@/components/pages/closing/ClosingKandangList'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; @@ -27,7 +26,6 @@ interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; salesData?: BaseClosingSales; - hppExpeditionData?: ClosingHppExpedition; projectData?: ProjectFlock; kandangData?: ProjectFlockKandang; } @@ -36,7 +34,6 @@ const ClosingDetail: React.FC = ({ id, initialValue, salesData, - hppExpeditionData, projectData, kandangData, }) => { @@ -79,9 +76,7 @@ const ClosingDetail: React.FC = ({ { id: 'hppEkspedisi', label: 'HPP Ekspedisi', - content: ( - - ), + content: , }, { id: 'dataProduksi', diff --git a/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx b/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx new file mode 100644 index 00000000..ad7f0ec1 --- /dev/null +++ b/src/components/pages/closing/tab/HppExpeditionClosingTab.tsx @@ -0,0 +1,19 @@ +import HppExpeditionClosingTable from '@/components/pages/closing/table/HppExpeditionClosingTable'; + +interface HppExpeditionClosingTabProps { + projectFlockId: number; +} + +const HppExpeditionClosingTab = ({ + projectFlockId, +}: HppExpeditionClosingTabProps) => { + return ( +
+ {projectFlockId && ( + + )} +
+ ); +}; + +export default HppExpeditionClosingTab; diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx index 2229180e..6c19c2a8 100644 --- a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -5,27 +5,49 @@ import { ColumnDef } from '@tanstack/react-table'; import Table from '@/components/Table'; import Card from '@/components/Card'; import { formatCurrency } from '@/lib/helper'; -import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { BaseExpeditionCost } from '@/types/api/closing'; +import { ClosingApi } from '@/services/api/closing'; +import { useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; interface HppExpeditionClosingTableProps { - type?: 'detail'; - initialValues?: BaseHppExpedition; + projectFlockId: number; } const HppExpeditionClosingTable = ({ - initialValues, + projectFlockId, }: HppExpeditionClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: hppExpedition, isLoading } = useSWR( + kandangId + ? `/closing/hpp-expedition/${projectFlockId}/${kandangId}` + : `/closing/hpp-expedition/${projectFlockId}`, + () => + kandangId + ? ClosingApi.getHppEkspedisiByKandang(projectFlockId, Number(kandangId)) + : ClosingApi.getHppEkspedisi(projectFlockId) + ); + const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => { - return initialValues?.expedition_costs || []; - }, [initialValues]); + if (isResponseSuccess(hppExpedition)) { + return hppExpedition.data.expedition_costs || []; + } + return []; + }, [hppExpedition]); const totals = useMemo(() => { - const totalHpp = initialValues?.total_hpp_amount || 0; - + if (isResponseSuccess(hppExpedition)) { + return { + totalHpp: hppExpedition.data.total_hpp_amount || 0, + }; + } return { - totalHpp, + totalHpp: 0, }; - }, [initialValues]); + }, [hppExpedition]); const costOfRevenueExpeditionColumns: ColumnDef[] = useMemo( @@ -81,6 +103,7 @@ const HppExpeditionClosingTable = ({ 0} className={{ tableWrapperClassName: 'overflow-x-auto', From d9bd73d8c1e88eee02fad0bc1a2954bdbf6daa24 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:32:33 +0700 Subject: [PATCH 113/149] refactor(FE): Refactor sales data fetching and component structure --- src/app/closing/detail/page.tsx | 18 +------- .../pages/closing/ClosingDetailTabs.tsx | 9 ++-- .../pages/closing/tab/SalesClosingTab.tsx | 19 +++++++++ .../pages/closing/table/SalesClosingTable.tsx | 42 +++++++++++++++---- 4 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 src/components/pages/closing/tab/SalesClosingTab.tsx diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index d081951c..c8d5c47e 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -34,18 +34,6 @@ const ClosingDetailPage = () => { () => ProjectFlockKandangApi.getSingle(Number(kandangId)) ); - const { data: salesData, isLoading: isLoadingSales } = useSWR( - kandangId - ? `sales-${closingId}-${kandangId}` - : closingId - ? `sales-${closingId}` - : null, - () => - kandangId - ? ClosingApi.getPenjualanByKandang(Number(closingId), Number(kandangId)) - : ClosingApi.getPenjualan(Number(closingId)) - ); - if (!closingId) { router.back(); @@ -62,10 +50,7 @@ const ClosingDetailPage = () => { } const isLoading = - isLoadingClosing || - isLoadingSales || - isLoadingProject || - isLoadingKandang; + isLoadingClosing || isLoadingProject || isLoadingKandang; return (
@@ -75,7 +60,6 @@ const ClosingDetailPage = () => { = ({ id, initialValue, - salesData, projectData, kandangData, }) => { @@ -60,7 +57,7 @@ const ClosingDetail: React.FC = ({ { id: 'penjualan', label: 'Penjualan', - content: , + content: , }, { id: 'overhead', @@ -91,7 +88,7 @@ const ClosingDetail: React.FC = ({ ]; return validTabs; - }, [initialValue, salesData, kandangData, id]); + }, [initialValue, kandangData, id]); return ( <> diff --git a/src/components/pages/closing/tab/SalesClosingTab.tsx b/src/components/pages/closing/tab/SalesClosingTab.tsx new file mode 100644 index 00000000..23ec720b --- /dev/null +++ b/src/components/pages/closing/tab/SalesClosingTab.tsx @@ -0,0 +1,19 @@ +import SalesClosingTable from '@/components/pages/closing/table/SalesClosingTable'; + +interface SalesClosingTabProps { + projectFlockId: number; +} + +const SalesClosingTab = ({ + projectFlockId, +}: SalesClosingTabProps) => { + return ( +
+ {projectFlockId && ( + + )} +
+ ); +}; + +export default SalesClosingTab; diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index b30cfb4a..6ad716b2 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -5,6 +5,7 @@ import { ColumnDef } from '@tanstack/react-table'; import Table from '@/components/Table'; import Card from '@/components/Card'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; import { BaseClosingSales, BaseSales, @@ -13,20 +14,46 @@ import { import { Product } from '@/types/api/master-data/product'; import { Customer } from '@/types/api/master-data/customer'; import { Kandang } from '@/types/api/master-data/kandang'; +import { ClosingApi } from '@/services/api/closing'; +import { useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; interface SalesClosingTableProps { - type?: 'detail'; - initialValues?: BaseClosingSales; + projectFlockId: number; } -const SalesClosingTable = ({ initialValues }: SalesClosingTableProps) => { +const SalesClosingTable = ({ + projectFlockId, +}: SalesClosingTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + + const { data: sales, isLoading } = useSWR( + kandangId + ? `/closing/sales/${projectFlockId}/${kandangId}` + : `/closing/sales/${projectFlockId}`, + () => + kandangId + ? ClosingApi.getPenjualanByKandang( + projectFlockId, + Number(kandangId) + ) + : ClosingApi.getPenjualan(projectFlockId) + ); + const salesData: BaseSales[] = useMemo(() => { - return initialValues?.sales || []; - }, [initialValues]); + if (isResponseSuccess(sales)) { + return sales.data.sales || []; + } + return []; + }, [sales]); const summary: ClosingSalesSummary | undefined = useMemo(() => { - return initialValues?.summary; - }, [initialValues]); + if (isResponseSuccess(sales)) { + return sales.data.summary; + } + return undefined; + }, [sales]); const totals = useMemo(() => { if (salesData.length === 0) { @@ -306,6 +333,7 @@ const SalesClosingTable = ({ initialValues }: SalesClosingTableProps) => {
0} className={{ tableWrapperClassName: 'overflow-x-auto', From 1fe722cb81362f371d3e251f2fdf991a993b88ea Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:33:31 +0700 Subject: [PATCH 114/149] refactor(FE): Refactor code formatting for consistency and readability --- src/app/closing/detail/page.tsx | 3 +-- src/components/pages/closing/ClosingDetailTabs.tsx | 4 +--- src/components/pages/closing/tab/SalesClosingTab.tsx | 8 ++------ src/components/pages/closing/table/SalesClosingTable.tsx | 9 ++------- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index c8d5c47e..96487258 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -49,8 +49,7 @@ const ClosingDetailPage = () => { return; } - const isLoading = - isLoadingClosing || isLoadingProject || isLoadingKandang; + const isLoading = isLoadingClosing || isLoadingProject || isLoadingKandang; return (
diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index 3b9e7dce..59fea1ba 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -9,9 +9,7 @@ import ClosingGeneralInformationTable from '@/components/pages/closing/table/Clo import SapronakClosingTab from '@/components/pages/closing/tab/SapronakClosingTab'; import ProductionDataClosingTab from '@/components/pages/closing/tab/ProductionDataClosingTab'; -import { - ClosingGeneralInformation, -} from '@/types/api/closing'; +import { ClosingGeneralInformation } from '@/types/api/closing'; import SapronakCalculationClosingTab from '@/components/pages/closing/tab/SapronakCalculationClosingTab'; import OverheadClosingTab from '@/components/pages/closing/tab/OverheadClosingTab'; import FinanceClosingTab from '@/components/pages/closing/tab/FinanceClosingTab'; diff --git a/src/components/pages/closing/tab/SalesClosingTab.tsx b/src/components/pages/closing/tab/SalesClosingTab.tsx index 23ec720b..ee343da0 100644 --- a/src/components/pages/closing/tab/SalesClosingTab.tsx +++ b/src/components/pages/closing/tab/SalesClosingTab.tsx @@ -4,14 +4,10 @@ interface SalesClosingTabProps { projectFlockId: number; } -const SalesClosingTab = ({ - projectFlockId, -}: SalesClosingTabProps) => { +const SalesClosingTab = ({ projectFlockId }: SalesClosingTabProps) => { return (
- {projectFlockId && ( - - )} + {projectFlockId && }
); }; diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index 6ad716b2..0d8a31ba 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -22,9 +22,7 @@ interface SalesClosingTableProps { projectFlockId: number; } -const SalesClosingTable = ({ - projectFlockId, -}: SalesClosingTableProps) => { +const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { const searchParams = useSearchParams(); const kandangId = searchParams.get('kandangId'); @@ -34,10 +32,7 @@ const SalesClosingTable = ({ : `/closing/sales/${projectFlockId}`, () => kandangId - ? ClosingApi.getPenjualanByKandang( - projectFlockId, - Number(kandangId) - ) + ? ClosingApi.getPenjualanByKandang(projectFlockId, Number(kandangId)) : ClosingApi.getPenjualan(projectFlockId) ); From c53430fa1fec86f2186db2227b23ae7780159072 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:35:42 +0700 Subject: [PATCH 115/149] refactor(FE): Organize Sapronak tables into a dedicated folder --- src/components/pages/closing/tab/SapronakClosingTab.tsx | 8 ++++---- .../{ => sapronak}/IncomingSapronaksSummaryTable.tsx | 0 .../table/{ => sapronak}/IncomingSapronaksTable.tsx | 0 .../{ => sapronak}/OutgoingSapronaksSummaryTable.tsx | 0 .../table/{ => sapronak}/OutgoingSapronaksTable.tsx | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/components/pages/closing/table/{ => sapronak}/IncomingSapronaksSummaryTable.tsx (100%) rename src/components/pages/closing/table/{ => sapronak}/IncomingSapronaksTable.tsx (100%) rename src/components/pages/closing/table/{ => sapronak}/OutgoingSapronaksSummaryTable.tsx (100%) rename src/components/pages/closing/table/{ => sapronak}/OutgoingSapronaksTable.tsx (100%) diff --git a/src/components/pages/closing/tab/SapronakClosingTab.tsx b/src/components/pages/closing/tab/SapronakClosingTab.tsx index 8f931671..1a5d26dd 100644 --- a/src/components/pages/closing/tab/SapronakClosingTab.tsx +++ b/src/components/pages/closing/tab/SapronakClosingTab.tsx @@ -1,9 +1,9 @@ 'use client'; -import IncomingSapronaksTable from '@/components/pages/closing/table/IncomingSapronaksTable'; -import OutgoingSapronaksTable from '@/components/pages/closing/table/OutgoingSapronaksTable'; -import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/IncomingSapronaksSummaryTable'; -import ClosingOutgoingSapronaksSummaryTable from '../table/OutgoingSapronaksSummaryTable'; +import IncomingSapronaksTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksTable'; +import OutgoingSapronaksTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksTable'; +import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable'; +import ClosingOutgoingSapronaksSummaryTable from '../table/sapronak/OutgoingSapronaksSummaryTable'; interface SapronakClosingTabProps { projectFlockId?: number; diff --git a/src/components/pages/closing/table/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx similarity index 100% rename from src/components/pages/closing/table/IncomingSapronaksSummaryTable.tsx rename to src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx diff --git a/src/components/pages/closing/table/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx similarity index 100% rename from src/components/pages/closing/table/IncomingSapronaksTable.tsx rename to src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx diff --git a/src/components/pages/closing/table/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx similarity index 100% rename from src/components/pages/closing/table/OutgoingSapronaksSummaryTable.tsx rename to src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx diff --git a/src/components/pages/closing/table/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx similarity index 100% rename from src/components/pages/closing/table/OutgoingSapronaksTable.tsx rename to src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx From 9c953ca3825a873aace5a54deade8f1967d8fd5d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 09:37:54 +0700 Subject: [PATCH 116/149] refactor(FE): Fix incorrect import and component usage in SapronakClosingTab --- src/components/pages/closing/tab/SapronakClosingTab.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/tab/SapronakClosingTab.tsx b/src/components/pages/closing/tab/SapronakClosingTab.tsx index 1a5d26dd..21bb3b3f 100644 --- a/src/components/pages/closing/tab/SapronakClosingTab.tsx +++ b/src/components/pages/closing/tab/SapronakClosingTab.tsx @@ -3,7 +3,7 @@ import IncomingSapronaksTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksTable'; import OutgoingSapronaksTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksTable'; import IncomingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable'; -import ClosingOutgoingSapronaksSummaryTable from '../table/sapronak/OutgoingSapronaksSummaryTable'; +import OutgoingSapronaksSummaryTable from '@/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable'; interface SapronakClosingTabProps { projectFlockId?: number; @@ -20,9 +20,7 @@ const SapronakClosingTab = ({ projectFlockId }: SapronakClosingTabProps) => { - + )}
From 8fe19feaac1039f6e47ce8c6a4d0d6bdbbbf81f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:03:25 +0700 Subject: [PATCH 117/149] feat(FE): Add skeleton components for closing pages --- .../closing/skeleton/ClosingTabSkeleton.tsx | 36 + .../skeleton/FinanceClosingSkeleton.tsx | 40 ++ .../skeleton/HppExpeditionClosingSkeleton.tsx | 44 ++ .../skeleton/OverheadClosingSkeleton.tsx | 72 ++ .../ProductionDataClosingSkeleton.tsx | 33 + .../closing/skeleton/SalesClosingSkeleton.tsx | 84 +++ .../SapronakCalculationClosingSkeleton.tsx | 72 ++ .../skeleton/SapronakClosingSkeleton.tsx | 126 ++++ .../closing/tab/ProductionDataClosingTab.tsx | 15 +- .../closing/table/FinanceClosingTable.tsx | 619 +++++++++--------- .../table/HppExpeditionClosingTable.tsx | 56 +- .../closing/table/OverheadClosingTable.tsx | 178 ++--- .../pages/closing/table/SalesClosingTable.tsx | 52 +- .../table/SapronakCalculationClosingTable.tsx | 124 ++-- .../IncomingSapronaksSummaryTable.tsx | 81 ++- .../table/sapronak/IncomingSapronaksTable.tsx | 81 ++- .../OutgoingSapronaksSummaryTable.tsx | 81 ++- .../table/sapronak/OutgoingSapronaksTable.tsx | 81 ++- 18 files changed, 1262 insertions(+), 613 deletions(-) create mode 100644 src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx create mode 100644 src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx diff --git a/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx new file mode 100644 index 00000000..821ce72a --- /dev/null +++ b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx @@ -0,0 +1,36 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; + +const ClosingTabSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
[]} + isLoading={true} + className={{ + skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4', + headerColumnClassName: 'whitespace-nowrap', + containerClassName: 'mb-0 overflow-hidden', + tableWrapperClassName: 'overflow-hidden', + }} + /> +
+ +
+ + ); +}; + +export default ClosingTabSkeleton; diff --git a/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx b/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx new file mode 100644 index 00000000..1168710c --- /dev/null +++ b/src/components/pages/closing/skeleton/FinanceClosingSkeleton.tsx @@ -0,0 +1,40 @@ +import { Icon } from '@iconify/react'; +import Card from '@/components/Card'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; + +const FinanceClosingSkeleton = ({ + title = 'Data Keuangan Belum Tersedia', + subtitle = 'Tidak ada data keuangan untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( + +
+ + } + title={title} + description={subtitle} + /> +
+
+ ); +}; + +export default FinanceClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx new file mode 100644 index 00000000..490839e1 --- /dev/null +++ b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx @@ -0,0 +1,44 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { BaseExpeditionCost } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const HppExpeditionClosingSkeleton = ({ + columns, + title = 'Data HPP Ekspedisi Belum Tersedia', + subtitle = 'Tidak ada data HPP ekspedisi untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultColumns: ColumnDef[] = [ + { + id: 'id', + header: 'No', + }, + { + id: 'expedition_vendor_name', + header: 'Nama Ekspedisi', + }, + { + id: 'hpp_amount', + header: 'HPP Ekspedisi', + }, + ]; + + return ( + + columns={columns || defaultColumns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default HppExpeditionClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx new file mode 100644 index 00000000..0ac2f4f3 --- /dev/null +++ b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx @@ -0,0 +1,72 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { Overhead } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const OverheadClosingSkeleton = ({ + columns, + title = 'Data Overhead Belum Tersedia', + subtitle = 'Tidak ada data overhead untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultColumns: ColumnDef[] = [ + { + id: 'name', + header: 'Nama Overhead', + }, + { + id: 'budget_quantity', + header: 'Budget Pengajuan - Jumlah', + }, + { + id: 'budget_unit_price', + header: 'Budget Pengajuan - Harga Satuan', + }, + { + id: 'budget_total_amount', + header: 'Budget Pengajuan - Total', + }, + { + id: 'actual_quantity', + header: 'Realisasi - Jumlah', + }, + { + id: 'actual_unit_price', + header: 'Realisasi - Harga Satuan', + }, + { + id: 'actual_total_amount', + header: 'Realisasi - Total', + }, + { + id: 'difference_quantity', + header: 'Selisih - Jumlah', + }, + { + id: 'difference_unit_price', + header: 'Selisih - Harga Satuan', + }, + { + id: 'difference_total_amount', + header: 'Selisih - Total', + }, + ]; + + return ( + + columns={columns || defaultColumns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default OverheadClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx b/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx new file mode 100644 index 00000000..e0031394 --- /dev/null +++ b/src/components/pages/closing/skeleton/ProductionDataClosingSkeleton.tsx @@ -0,0 +1,33 @@ +import { Icon } from '@iconify/react'; +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; + +const ProductionDataClosingSkeleton = ({ + title = 'Data Produksi Belum Tersedia', + subtitle = 'Tidak ada data produksi untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + title?: string; + subtitle?: string; + iconName?: string; +}) => { + return ( +
+
+ + } + title={title} + description={subtitle} + /> +
+
+ ); +}; + +export default ProductionDataClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx new file mode 100644 index 00000000..c0ae3a28 --- /dev/null +++ b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx @@ -0,0 +1,84 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { BaseSales } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SalesClosingSkeleton = ({ + columns, + title = 'Data Penjualan Belum Tersedia', + subtitle = 'Tidak ada data penjualan untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultColumns: ColumnDef[] = [ + { + id: 'realization_date', + header: 'Tanggal Realisasi', + }, + { + id: 'age', + header: 'Umur', + }, + { + id: 'do_number', + header: 'No. DO', + }, + { + id: 'product', + header: 'Produk', + }, + { + id: 'customer', + header: 'Customer', + }, + { + id: 'qty', + header: 'Kuantitas', + }, + { + id: 'weight', + header: 'Kg', + }, + { + id: 'avg_weight', + header: 'AVG (Kg)', + }, + { + id: 'sales_price', + header: 'Harga Sales (Rp)', + }, + { + id: 'total_sales_price', + header: 'Total Sales (Rp)', + }, + { + id: 'actual_price', + header: 'Harga Act (Rp)', + }, + { + id: 'total_actual_price', + header: 'Total Act (Rp)', + }, + { + id: 'kandang', + header: 'Kandang', + }, + ]; + + return ( + + columns={columns || defaultColumns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default SalesClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx new file mode 100644 index 00000000..6c1b9f3c --- /dev/null +++ b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx @@ -0,0 +1,72 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { RowSapronakCalculation } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SapronakCalculationClosingSkeleton = ({ + columns, + title = 'Data Perhitungan Sapronak Belum Tersedia', + subtitle = 'Tidak ada data perhitungan sapronak untuk periode ini.', + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultColumns: ColumnDef[] = [ + { + id: 'date', + header: 'Tanggal', + }, + { + id: 'reference_number', + header: 'No. Referensi', + }, + { + id: 'qty_in', + header: 'QTY Masuk', + }, + { + id: 'qty_out', + header: 'QTY Keluar', + }, + { + id: 'qty_used', + header: 'QTY Pakai', + }, + { + id: 'balance', + header: 'Saldo', + }, + { + id: 'unit_price_in', + header: 'Harga Masuk', + }, + { + id: 'unit_price_out', + header: 'Harga Keluar', + }, + { + id: 'total_price_in', + header: 'Total Harga Masuk', + }, + { + id: 'total_price_out', + header: 'Total Harga Keluar', + }, + ]; + + return ( + + columns={columns || defaultColumns} + icon={ + + } + title={title} + subtitle={subtitle} + /> + ); +}; + +export default SapronakCalculationClosingSkeleton; diff --git a/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx new file mode 100644 index 00000000..e3ea211b --- /dev/null +++ b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx @@ -0,0 +1,126 @@ +import { Icon } from '@iconify/react'; +import ClosingTabSkeleton from './ClosingTabSkeleton'; +import { ClosingIncomingSapronak } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const SapronakClosingSkeleton = ({ + columns, + type = 'incoming', + title, + subtitle, + iconName = 'heroicons:chart-bar', +}: { + columns?: ColumnDef[]; + type?: 'incoming' | 'outgoing'; + title?: string; + subtitle?: string; + iconName?: string; +}) => { + const defaultIncomingColumns: ColumnDef[] = [ + { + id: '#', + header: '#', + }, + { + id: 'date', + header: 'Tanggal', + }, + { + id: 'reference_number', + header: 'No. Referensi', + }, + { + id: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + id: 'product_name', + header: 'Produk', + }, + { + id: 'product_category', + header: 'Kategori Produk', + }, + { + id: 'source_warehouse', + header: 'Gudang Asal', + }, + { + id: 'destination_warehouse', + header: 'Gudang Tujuan', + }, + { + id: 'quantity', + header: 'Kuantitas', + }, + { + id: 'notes', + header: 'Keterangan', + }, + ]; + + const defaultOutgoingColumns: ColumnDef[] = [ + { + id: '#', + header: '#', + }, + { + id: 'date', + header: 'Tanggal', + }, + { + id: 'reference_number', + header: 'No. Referensi', + }, + { + id: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + id: 'product_name', + header: 'Produk', + }, + { + id: 'product_category', + header: 'Kategori Produk', + }, + { + id: 'source_warehouse', + header: 'Gudang Asal', + }, + { + id: 'quantity', + header: 'Kuantitas', + }, + { + id: 'notes', + header: 'Keterangan', + }, + ]; + + const defaultTitle = + type === 'incoming' + ? 'Data Sapronak Masuk Belum Tersedia' + : 'Data Sapronak Keluar Belum Tersedia'; + + const defaultSubtitle = + type === 'incoming' + ? 'Silakan pilih periode atau filter untuk melihat data sapronak masuk.' + : 'Silakan pilih periode atau filter untuk melihat data sapronak keluar.'; + + return ( + + columns={ + columns || + (type === 'incoming' ? defaultIncomingColumns : defaultOutgoingColumns) + } + icon={ + + } + title={title || defaultTitle} + subtitle={subtitle || defaultSubtitle} + /> + ); +}; + +export default SapronakClosingSkeleton; diff --git a/src/components/pages/closing/tab/ProductionDataClosingTab.tsx b/src/components/pages/closing/tab/ProductionDataClosingTab.tsx index eebccae3..3647dc1f 100644 --- a/src/components/pages/closing/tab/ProductionDataClosingTab.tsx +++ b/src/components/pages/closing/tab/ProductionDataClosingTab.tsx @@ -5,6 +5,7 @@ import useSWR from 'swr'; import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess } from '@/lib/api-helper'; import { formatNumber } from '@/lib/helper'; +import ProductionDataClosingSkeleton from '@/components/pages/closing/skeleton/ProductionDataClosingSkeleton'; interface ProductionDataClosingTabProps { projectFlockId: number; @@ -22,18 +23,16 @@ const ProductionDataClosingTab = ({ ); if (isLoading) { - return ( -
- -
- ); + return ; } if (!productionData || !isResponseSuccess(productionData)) { return ( -
- Gagal memuat data produksi. -
+ ); } diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx index 8dce38de..de4d6e47 100644 --- a/src/components/pages/closing/table/FinanceClosingTable.tsx +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -7,6 +7,7 @@ import { HppItem, ProfitLossItem } from '@/types/api/closing'; import { useSearchParams } from 'next/navigation'; import { useMemo } from 'react'; import useSWR from 'swr'; +import FinanceClosingSkeleton from '@/components/pages/closing/skeleton/FinanceClosingSkeleton'; const FinanceClosingTable = ({ projectFlockId, @@ -82,316 +83,336 @@ const FinanceClosingTable = ({ return (
- <> - -
-
-
Laba Rugi Brutto
-
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.gross_profit.amount - ) - : '-'} + {isLoading ? ( + + ) : !isResponseSuccess(finance) ? ( + + ) : ( + <> + +
+
+
Laba Rugi Brutto
+
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.gross_profit.amount + ) + : '-'} +
+
+
+
Laba Rugi Netto
+
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit.amount + ) + : '-'} +
-
-
Laba Rugi Netto
-
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit.amount - ) - : '-'} -
-
-
- - -
- - data={hppTableData} - isLoading={isLoading} - columns={[ - { - header: 'No.', - enableSorting: false, - accessorFn: (item, index) => { - if (item.code === 'custom_row') return '-'; - const dataRowsBefore = hppTableData - .slice(0, index) - .filter((row) => row.code !== 'custom_row').length; - return dataRowsBefore + 1; + + +
+ + data={hppTableData} + isLoading={isLoading} + columns={[ + { + header: 'No.', + enableSorting: false, + accessorFn: (item, index) => { + if (item.code === 'custom_row') return '-'; + const dataRowsBefore = hppTableData + .slice(0, index) + .filter((row) => row.code !== 'custom_row').length; + return dataRowsBefore + 1; + }, + footer: (props) => { + return 'HPP'; + }, }, - footer: (props) => { - return 'HPP'; + { + header: 'Jenis', + enableSorting: false, + accessorFn: (item) => formatTitleCase(item.label || '-'), }, - }, - { - header: 'Jenis', - enableSorting: false, - accessorFn: (item) => formatTitleCase(item.label || '-'), - }, - { - header: 'Budgeting', - enableSorting: false, - columns: [ - { - header: 'Rp/Ekor', - id: 'budgeting_rp_per_bird', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.rp_per_bird || 0), - footer: (props) => { - return props.column.id === 'budgeting_rp_per_bird' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting - ?.rp_per_bird || 0 - ) - : '-'; + { + header: 'Budgeting', + enableSorting: false, + columns: [ + { + header: 'Rp/Ekor', + id: 'budgeting_rp_per_bird', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.rp_per_bird || 0), + footer: (props) => { + return props.column.id === 'budgeting_rp_per_bird' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting + ?.rp_per_bird || 0 + ) + : '-'; + }, }, - }, - { - header: 'Rp/Kg', - id: 'budgeting_rp_per_kg', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.rp_per_kg || 0), - footer: (props) => { - return props.column.id === 'budgeting_rp_per_kg' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting?.rp_per_kg || - 0 - ) - : '-'; + { + header: 'Rp/Kg', + id: 'budgeting_rp_per_kg', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.rp_per_kg || 0), + footer: (props) => { + return props.column.id === 'budgeting_rp_per_kg' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting + ?.rp_per_kg || 0 + ) + : '-'; + }, }, - }, - { - header: 'Jumlah (Rp)', - id: 'budgeting_amount', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.budgeting?.amount || 0), - footer: (props) => { - return props.column.id === 'budgeting_amount' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.budgeting?.amount || 0 - ) - : '-'; + { + header: 'Jumlah (Rp)', + id: 'budgeting_amount', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.budgeting?.amount || 0), + footer: (props) => { + return props.column.id === 'budgeting_amount' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.budgeting?.amount || 0 + ) + : '-'; + }, }, - }, - ], - }, - { - header: 'Realization', - enableSorting: false, - columns: [ - { - header: 'Rp/Ekor', - id: 'realization_rp_per_bird', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.rp_per_bird || 0), - footer: (props) => { - return props.column.id === 'realization_rp_per_bird' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization - ?.rp_per_bird || 0 - ) - : '-'; + ], + }, + { + header: 'Realization', + enableSorting: false, + columns: [ + { + header: 'Rp/Ekor', + id: 'realization_rp_per_bird', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.rp_per_bird || 0), + footer: (props) => { + return props.column.id === + 'realization_rp_per_bird' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization + ?.rp_per_bird || 0 + ) + : '-'; + }, }, - }, - { - header: 'Rp/Kg', - id: 'realization_rp_per_kg', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.rp_per_kg || 0), - footer: (props) => { - return props.column.id === 'realization_rp_per_kg' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization - ?.rp_per_kg || 0 - ) - : '-'; + { + header: 'Rp/Kg', + id: 'realization_rp_per_kg', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.rp_per_kg || 0), + footer: (props) => { + return props.column.id === 'realization_rp_per_kg' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization + ?.rp_per_kg || 0 + ) + : '-'; + }, }, - }, - { - header: 'Jumlah (Rp)', - id: 'realization_amount', - enableSorting: false, - accessorFn: (item) => - formatCurrency(item.realization?.amount || 0), - footer: (props) => { - return props.column.id === 'realization_amount' && - isResponseSuccess(finance) - ? formatCurrency( - finance.data.hpp.summary?.realization?.amount || 0 - ) - : '-'; + { + header: 'Jumlah (Rp)', + id: 'realization_amount', + enableSorting: false, + accessorFn: (item) => + formatCurrency(item.realization?.amount || 0), + footer: (props) => { + return props.column.id === 'realization_amount' && + isResponseSuccess(finance) + ? formatCurrency( + finance.data.hpp.summary?.realization?.amount || + 0 + ) + : '-'; + }, }, - }, - ], - }, - ]} - renderCustomRow={(row) => { - const rowData = row.original; - if (rowData.code === 'custom_row') { - return ( -
- - -
- {formatTitleCase(rowData.label ?? '-')} -
- - - ); - } - return null; - }} - renderFooter={isResponseSuccess(finance)} - /> - - - -
- - data={profitLossTableData} - isLoading={isLoading} - columns={[ - { - header: 'Jenis', - enableSorting: false, - accessorFn: (item) => item.label, - cell: (item) => ( -
- {formatTitleCase(item.row.original.label || '-')} -
- ), - footer: () => ( -
LABA RUGI NETTO
- ), - }, - { - header: 'Rp/Ekor', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .rp_per_bird || 0 - ) - : formatCurrency(0)} -
- ), - }, - { - header: 'Rp/Kg', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .rp_per_kg || 0 - ) - : formatCurrency(0)} -
- ), - }, - { - header: 'Jumlah (Rp)', - enableSorting: false, - accessorFn: (item) => formatCurrency(item.amount || 0), - footer: () => ( -
- {isResponseSuccess(finance) - ? formatCurrency( - finance.data.profit_loss.summary.net_profit - .amount || 0 - ) - : formatCurrency(0)} -
- ), - }, - ]} - renderCustomRow={(row) => { - const rowData = row.original; - if (rowData.code === 'custom_row') { - return ( -
- - - - - - ); - } - return null; - }} - className={{ - paginationClassName: 'hidden', - }} - renderFooter={isResponseSuccess(finance)} - /> - - - + + + + ); + } + return null; + }} + renderFooter={isResponseSuccess(finance)} + /> + + + +
+ + data={profitLossTableData} + isLoading={isLoading} + columns={[ + { + header: 'Jenis', + enableSorting: false, + accessorFn: (item) => item.label, + cell: (item) => ( +
+ {formatTitleCase(item.row.original.label || '-')} +
+ ), + footer: () => ( +
LABA RUGI NETTO
+ ), + }, + { + header: 'Rp/Ekor', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .rp_per_bird || 0 + ) + : formatCurrency(0)} +
+ ), + }, + { + header: 'Rp/Kg', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .rp_per_kg || 0 + ) + : formatCurrency(0)} +
+ ), + }, + { + header: 'Jumlah (Rp)', + enableSorting: false, + accessorFn: (item) => formatCurrency(item.amount || 0), + footer: () => ( +
+ {isResponseSuccess(finance) + ? formatCurrency( + finance.data.profit_loss.summary.net_profit + .amount || 0 + ) + : formatCurrency(0)} +
+ ), + }, + ]} + renderCustomRow={(row) => { + const rowData = row.original; + if (rowData.code === 'custom_row') { + return ( +
+ + + + + + ); + } + return null; + }} + className={{ + paginationClassName: 'hidden', + }} + renderFooter={isResponseSuccess(finance)} + /> + + + + )} ); }; diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx index 6c19c2a8..a1ce4a94 100644 --- a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -10,6 +10,7 @@ import { BaseExpeditionCost } from '@/types/api/closing'; import { ClosingApi } from '@/services/api/closing'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; +import HppExpeditionClosingSkeleton from '@/components/pages/closing/skeleton/HppExpeditionClosingSkeleton'; interface HppExpeditionClosingTableProps { projectFlockId: number; @@ -100,28 +101,39 @@ const HppExpeditionClosingTable = ({ body: 'p-0', }} > -
{ + const rowData = row.original; + if (rowData.code === 'custom_row') { + return ( +
-
- {formatTitleCase(rowData.label ?? '-')} -
-
-
- {formatCurrency(rowData.rp_per_bird ?? 0)} -
-
-
- {formatCurrency(rowData.rp_per_kg ?? 0)} -
-
-
- {formatCurrency(rowData.amount ?? 0)} -
-
+
+ {formatTitleCase(rowData.label ?? '-')} +
+
+
+ {formatTitleCase(rowData.label ?? '-')} +
+
+
+ {formatCurrency(rowData.rp_per_bird ?? 0)} +
+
+
+ {formatCurrency(rowData.rp_per_kg ?? 0)} +
+
+
+ {formatCurrency(rowData.amount ?? 0)} +
+
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> + {isLoading ? ( + + ) : costOfRevenueExpeditionData.length === 0 ? ( + + ) : ( +
0} + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index f082697a..b1f64f64 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -14,6 +14,7 @@ import { ColumnDef } from '@tanstack/react-table'; import { useSearchParams } from 'next/navigation'; import { useMemo } from 'react'; import useSWR from 'swr'; +import OverheadClosingSkeleton from '@/components/pages/closing/skeleton/OverheadClosingSkeleton'; interface OverheadClosingTableProps { projectFlockId: number; @@ -209,95 +210,108 @@ const OverheadClosingTable = ({ return ( <> - - - data={ - kandangId - ? isResponseSuccess(overheadKandang) - ? (overheadKandang.data?.overheads ?? []) - : [] - : isResponseSuccess(overhead) - ? (overhead.data?.overheads ?? []) - : [] - } + {isLoadingOverhead ? ( + + ) : !isResponseSuccess(overhead) || + (!kandangId && overhead.data?.overheads.length === 0) || + (kandangId && !isResponseSuccess(overheadKandang)) ? ( + 0 - : false - } + iconName='heroicons:chart-bar' + title='Data Overhead Tidak Ditemukan' + subtitle='Tidak ada data overhead untuk periode ini.' /> - {kandangId && ( - + + data={ + kandangId + ? isResponseSuccess(overheadKandang) + ? (overheadKandang.data?.overheads ?? []) + : [] + : isResponseSuccess(overhead) + ? (overhead.data?.overheads ?? []) + : [] + } + columns={columns} className={{ - wrapper: 'w-full', - body: 'p-4 shadow-button-soft border border-base-content/10 rounded-lg', + containerClassName: 'my-4', + headerColumnClassName: cn( + TABLE_DEFAULT_STYLING.headerColumnClassName, + 'whitespace-nowrap' + ), }} - > -
-
-

Pembelian Kandang

-
-
- -
-
-
- Populasi Akhir KANDANG{' '} - Pemakaian - Di FARM + isLoading={isLoadingOverhead} + renderFooter={ + isResponseSuccess(overhead) + ? overhead.data?.overheads.length > 0 + : false + } + /> + {kandangId && ( + +
+
+

Pembelian Kandang

-
-
- Populasi Akhir Proyek +
+ +
+
+
+ Populasi Akhir KANDANG{' '} + {' '} + Pemakaian Di FARM +
+
+
+ Populasi Akhir Proyek +
+
+
+ +
+
+
+ {formatNumber(chickinPopulation ?? 0)} + + {formatCurrency( + isResponseSuccess(overhead) + ? overhead.data?.total.actual_total_amount + : 0 + )} +
+
+
+ {formatNumber(generalInformation?.population ?? 0)} +
+
+
+ +
+
+

+ {formatNumber(kandangTotal || 0)} +

-
- -
-
-
- {formatNumber(chickinPopulation ?? 0)} - - {formatCurrency( - isResponseSuccess(overhead) - ? overhead.data?.total.actual_total_amount - : 0 - )} -
-
-
- {formatNumber(generalInformation?.population ?? 0)} -
-
-
- -
-
-

- {formatNumber(kandangTotal || 0)} -

-
-
-
- )} - + + )} + + )} ); }; diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index 0d8a31ba..fc3d4c55 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -17,6 +17,7 @@ import { Kandang } from '@/types/api/master-data/kandang'; import { ClosingApi } from '@/services/api/closing'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; +import SalesClosingSkeleton from '@/components/pages/closing/skeleton/SalesClosingSkeleton'; interface SalesClosingTableProps { projectFlockId: number; @@ -325,27 +326,36 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { body: 'p-0', }} > -
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> + {isLoading ? ( + + ) : salesData.length === 0 ? ( + + ) : ( +
0} + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx index ba92c3d0..53174a71 100644 --- a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -15,6 +15,7 @@ import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess } from '@/lib/api-helper'; import { ClosingGeneralInformation } from '@/types/api/closing'; import { useSearchParams } from 'next/navigation'; +import SapronakCalculationClosingSkeleton from '@/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton'; interface SapronakCalculationClosingTableProps { projectFlockId: number; @@ -193,21 +194,32 @@ const SapronakCalculationClosingTable = ({ body: 'p-4 shadow', }} > - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.doc?.rows ?? []) - : [] - } - columns={docColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.doc?.rows.length > 0 - } - /> + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.doc?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.doc?.rows ?? []) + : [] + } + columns={docColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.doc?.rows?.length > 0 + } + /> + )} - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.ovk?.rows ?? []) - : [] - } - columns={ovkColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.ovk?.rows.length > 0 - } - /> + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.ovk?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.ovk?.rows ?? []) + : [] + } + columns={ovkColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.ovk?.rows?.length > 0 + } + /> + )} - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.pakan?.rows ?? []) - : [] - } - columns={pakanColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={ - isResponseSuccess(sapronakCalculation) && - sapronakCalculation.data?.pakan?.rows.length > 0 - } - /> + {isLoading ? ( + + ) : isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.pakan?.rows?.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(sapronakCalculation) + ? (sapronakCalculation.data?.pakan?.rows ?? []) + : [] + } + columns={pakanColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter={ + isResponseSuccess(sapronakCalculation) && + sapronakCalculation.data?.pakan?.rows?.length > 0 + } + /> + )} ); diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index 49e4f108..d9f4f33a 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -15,6 +15,7 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingIncomingSapronakSummary } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingIncomingSapronaksSummaryTableProps { projectFlockId: number; @@ -131,40 +132,52 @@ const ClosingIncomingSapronaksSummaryTable = ({ titleClassName='w-full p-0!' >
- - data={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.data - : [] - } - columns={incomingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingIncomingSapronakSummaries} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronakSummaries) && - incomingSapronakSummaries?.data?.length === 0, - }), - }} - /> + {isLoadingIncomingSapronakSummaries ? ( + + ) : isResponseSuccess(incomingSapronakSummaries) && + incomingSapronakSummaries.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronakSummaries} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(incomingSapronakSummaries) && + incomingSapronakSummaries?.data?.length === 0, + }), + }} + /> + )}
diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index 3d3a9d70..e8e88582 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -16,6 +16,7 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingIncomingSapronak } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingIncomingSapronaksTableProps { projectFlockId: number; @@ -167,40 +168,52 @@ const ClosingIncomingSapronaksTable = ({ - - data={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.data - : [] - } - columns={incomingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingIncomingSapronaks} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronaks) && - incomingSapronaks?.data?.length === 0, - }), - }} - /> + {isLoadingIncomingSapronaks ? ( + + ) : isResponseSuccess(incomingSapronaks) && + incomingSapronaks.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(incomingSapronaks) && + incomingSapronaks?.data?.length === 0, + }), + }} + /> + )} diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index 42fcb588..7acc6d67 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -15,6 +15,7 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingOutgoingSapronakSummary } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingOutgoingSapronaksSummaryTableProps { projectFlockId: number; @@ -131,40 +132,52 @@ const ClosingOutgoingSapronaksSummaryTable = ({ titleClassName='w-full p-0!' >
- - data={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.data - : [] - } - columns={outgoingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingOutgoingSapronakSummaries} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronakSummaries) && - outgoingSapronakSummaries?.data?.length === 0, - }), - }} - /> + {isLoadingOutgoingSapronakSummaries ? ( + + ) : isResponseSuccess(outgoingSapronakSummaries) && + outgoingSapronakSummaries.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronakSummaries} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(outgoingSapronakSummaries) && + outgoingSapronakSummaries?.data?.length === 0, + }), + }} + /> + )}
diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index acbbc52d..e6ab67db 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -16,6 +16,7 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; import { ClosingOutgoingSapronak } from '@/types/api/closing'; +import SapronakClosingSkeleton from '@/components/pages/closing/skeleton/SapronakClosingSkeleton'; interface ClosingOutgoingSapronaksTableProps { projectFlockId: number; @@ -167,40 +168,52 @@ const ClosingOutgoingSapronaksTable = ({ - - data={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.data - : [] - } - columns={outgoingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingOutgoingSapronaks} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronaks) && - outgoingSapronaks?.data?.length === 0, - }), - }} - /> + {isLoadingOutgoingSapronaks ? ( + + ) : isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks?.data?.length === 0, + }), + }} + /> + )} From befc1c12174a4a7f1bf419649e7d83a34039a168 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:14:28 +0700 Subject: [PATCH 118/149] refactor(FE): Refactor skeleton components to remove default columns --- .../closing/skeleton/ClosingTabSkeleton.tsx | 4 +- .../skeleton/HppExpeditionClosingSkeleton.tsx | 19 +--- .../skeleton/OverheadClosingSkeleton.tsx | 47 +-------- .../closing/skeleton/SalesClosingSkeleton.tsx | 59 +---------- .../SapronakCalculationClosingSkeleton.tsx | 47 +-------- .../skeleton/SapronakClosingSkeleton.tsx | 98 ++----------------- .../closing/table/OverheadClosingTable.tsx | 50 +++++----- .../table/SapronakCalculationClosingTable.tsx | 9 +- .../IncomingSapronaksSummaryTable.tsx | 3 +- .../table/sapronak/IncomingSapronaksTable.tsx | 3 +- .../OutgoingSapronaksSummaryTable.tsx | 3 +- .../table/sapronak/OutgoingSapronaksTable.tsx | 3 +- 12 files changed, 55 insertions(+), 290 deletions(-) diff --git a/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx index 821ce72a..44defca8 100644 --- a/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx +++ b/src/components/pages/closing/skeleton/ClosingTabSkeleton.tsx @@ -8,7 +8,7 @@ const ClosingTabSkeleton = ({ title, subtitle, }: { - columns: ColumnDef[]; + columns: ColumnDef[]; icon: React.ReactNode; title: string; subtitle: string; @@ -17,7 +17,7 @@ const ClosingTabSkeleton = ({
[]} + columns={columns} isLoading={true} className={{ skeletonCellClassName: 'animate-none w-full h-5 bg-base-content/4', diff --git a/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx index 490839e1..d9be9971 100644 --- a/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/HppExpeditionClosingSkeleton.tsx @@ -9,29 +9,14 @@ const HppExpeditionClosingSkeleton = ({ subtitle = 'Tidak ada data HPP ekspedisi untuk periode ini.', iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultColumns: ColumnDef[] = [ - { - id: 'id', - header: 'No', - }, - { - id: 'expedition_vendor_name', - header: 'Nama Ekspedisi', - }, - { - id: 'hpp_amount', - header: 'HPP Ekspedisi', - }, - ]; - return ( - columns={columns || defaultColumns} + columns={columns} icon={ } diff --git a/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx index 0ac2f4f3..7404f5d2 100644 --- a/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/OverheadClosingSkeleton.tsx @@ -9,57 +9,14 @@ const OverheadClosingSkeleton = ({ subtitle = 'Tidak ada data overhead untuk periode ini.', iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultColumns: ColumnDef[] = [ - { - id: 'name', - header: 'Nama Overhead', - }, - { - id: 'budget_quantity', - header: 'Budget Pengajuan - Jumlah', - }, - { - id: 'budget_unit_price', - header: 'Budget Pengajuan - Harga Satuan', - }, - { - id: 'budget_total_amount', - header: 'Budget Pengajuan - Total', - }, - { - id: 'actual_quantity', - header: 'Realisasi - Jumlah', - }, - { - id: 'actual_unit_price', - header: 'Realisasi - Harga Satuan', - }, - { - id: 'actual_total_amount', - header: 'Realisasi - Total', - }, - { - id: 'difference_quantity', - header: 'Selisih - Jumlah', - }, - { - id: 'difference_unit_price', - header: 'Selisih - Harga Satuan', - }, - { - id: 'difference_total_amount', - header: 'Selisih - Total', - }, - ]; - return ( - columns={columns || defaultColumns} + columns={columns} icon={ } diff --git a/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx index c0ae3a28..a9ec35aa 100644 --- a/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/SalesClosingSkeleton.tsx @@ -9,69 +9,14 @@ const SalesClosingSkeleton = ({ subtitle = 'Tidak ada data penjualan untuk periode ini.', iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultColumns: ColumnDef[] = [ - { - id: 'realization_date', - header: 'Tanggal Realisasi', - }, - { - id: 'age', - header: 'Umur', - }, - { - id: 'do_number', - header: 'No. DO', - }, - { - id: 'product', - header: 'Produk', - }, - { - id: 'customer', - header: 'Customer', - }, - { - id: 'qty', - header: 'Kuantitas', - }, - { - id: 'weight', - header: 'Kg', - }, - { - id: 'avg_weight', - header: 'AVG (Kg)', - }, - { - id: 'sales_price', - header: 'Harga Sales (Rp)', - }, - { - id: 'total_sales_price', - header: 'Total Sales (Rp)', - }, - { - id: 'actual_price', - header: 'Harga Act (Rp)', - }, - { - id: 'total_actual_price', - header: 'Total Act (Rp)', - }, - { - id: 'kandang', - header: 'Kandang', - }, - ]; - return ( - columns={columns || defaultColumns} + columns={columns} icon={ } diff --git a/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx index 6c1b9f3c..97d4a56c 100644 --- a/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/SapronakCalculationClosingSkeleton.tsx @@ -9,57 +9,14 @@ const SapronakCalculationClosingSkeleton = ({ subtitle = 'Tidak ada data perhitungan sapronak untuk periode ini.', iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultColumns: ColumnDef[] = [ - { - id: 'date', - header: 'Tanggal', - }, - { - id: 'reference_number', - header: 'No. Referensi', - }, - { - id: 'qty_in', - header: 'QTY Masuk', - }, - { - id: 'qty_out', - header: 'QTY Keluar', - }, - { - id: 'qty_used', - header: 'QTY Pakai', - }, - { - id: 'balance', - header: 'Saldo', - }, - { - id: 'unit_price_in', - header: 'Harga Masuk', - }, - { - id: 'unit_price_out', - header: 'Harga Keluar', - }, - { - id: 'total_price_in', - header: 'Total Harga Masuk', - }, - { - id: 'total_price_out', - header: 'Total Harga Keluar', - }, - ]; - return ( - columns={columns || defaultColumns} + columns={columns} icon={ } diff --git a/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx index e3ea211b..130cd846 100644 --- a/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx +++ b/src/components/pages/closing/skeleton/SapronakClosingSkeleton.tsx @@ -1,103 +1,20 @@ import { Icon } from '@iconify/react'; import ClosingTabSkeleton from './ClosingTabSkeleton'; -import { ClosingIncomingSapronak } from '@/types/api/closing'; import { ColumnDef } from '@tanstack/react-table'; -const SapronakClosingSkeleton = ({ +const SapronakClosingSkeleton = ({ columns, type = 'incoming', title, subtitle, iconName = 'heroicons:chart-bar', }: { - columns?: ColumnDef[]; + columns: ColumnDef[]; type?: 'incoming' | 'outgoing'; title?: string; subtitle?: string; iconName?: string; }) => { - const defaultIncomingColumns: ColumnDef[] = [ - { - id: '#', - header: '#', - }, - { - id: 'date', - header: 'Tanggal', - }, - { - id: 'reference_number', - header: 'No. Referensi', - }, - { - id: 'transaction_type', - header: 'Jenis Transaksi', - }, - { - id: 'product_name', - header: 'Produk', - }, - { - id: 'product_category', - header: 'Kategori Produk', - }, - { - id: 'source_warehouse', - header: 'Gudang Asal', - }, - { - id: 'destination_warehouse', - header: 'Gudang Tujuan', - }, - { - id: 'quantity', - header: 'Kuantitas', - }, - { - id: 'notes', - header: 'Keterangan', - }, - ]; - - const defaultOutgoingColumns: ColumnDef[] = [ - { - id: '#', - header: '#', - }, - { - id: 'date', - header: 'Tanggal', - }, - { - id: 'reference_number', - header: 'No. Referensi', - }, - { - id: 'transaction_type', - header: 'Jenis Transaksi', - }, - { - id: 'product_name', - header: 'Produk', - }, - { - id: 'product_category', - header: 'Kategori Produk', - }, - { - id: 'source_warehouse', - header: 'Gudang Asal', - }, - { - id: 'quantity', - header: 'Kuantitas', - }, - { - id: 'notes', - header: 'Keterangan', - }, - ]; - const defaultTitle = type === 'incoming' ? 'Data Sapronak Masuk Belum Tersedia' @@ -105,15 +22,12 @@ const SapronakClosingSkeleton = ({ const defaultSubtitle = type === 'incoming' - ? 'Silakan pilih periode atau filter untuk melihat data sapronak masuk.' - : 'Silakan pilih periode atau filter untuk melihat data sapronak keluar.'; + ? 'Tidak ada data sapronak masuk untuk periode ini.' + : 'Tidak ada data sapronak keluar untuk periode ini.'; return ( - - columns={ - columns || - (type === 'incoming' ? defaultIncomingColumns : defaultOutgoingColumns) - } + + columns={columns} icon={ } diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index b1f64f64..152c588c 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -210,27 +210,27 @@ const OverheadClosingTable = ({ return ( <> - {isLoadingOverhead ? ( - - ) : !isResponseSuccess(overhead) || - (!kandangId && overhead.data?.overheads.length === 0) || - (kandangId && !isResponseSuccess(overheadKandang)) ? ( - - ) : ( - + + {isLoadingOverhead ? ( + + ) : !isResponseSuccess(overhead) || + (!kandangId && overhead.data?.overheads.length === 0) || + (kandangId && !isResponseSuccess(overheadKandang)) ? ( + + ) : ( data={ kandangId @@ -256,7 +256,8 @@ const OverheadClosingTable = ({ : false } /> - {kandangId && ( + )} + {kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && ( - )} - - )} + )} + ); }; diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx index 53174a71..1e2f7534 100644 --- a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -195,10 +195,11 @@ const SapronakCalculationClosingTable = ({ }} > {isLoading ? ( - + ) : isResponseSuccess(sapronakCalculation) && sapronakCalculation.data?.doc?.rows?.length === 0 ? ( {isLoading ? ( - + ) : isResponseSuccess(sapronakCalculation) && sapronakCalculation.data?.ovk?.rows?.length === 0 ? ( {isLoading ? ( - + ) : isResponseSuccess(sapronakCalculation) && sapronakCalculation.data?.pakan?.rows?.length === 0 ? (
{isLoadingIncomingSapronakSummaries ? ( - + ) : isResponseSuccess(incomingSapronakSummaries) && incomingSapronakSummaries.data.length === 0 ? ( {isLoadingIncomingSapronaks ? ( - + ) : isResponseSuccess(incomingSapronaks) && incomingSapronaks.data.length === 0 ? (
{isLoadingOutgoingSapronakSummaries ? ( - + ) : isResponseSuccess(outgoingSapronakSummaries) && outgoingSapronakSummaries.data.length === 0 ? ( {isLoadingOutgoingSapronaks ? ( - + ) : isResponseSuccess(outgoingSapronaks) && outgoingSapronaks.data.length === 0 ? ( Date: Thu, 19 Feb 2026 10:15:20 +0700 Subject: [PATCH 119/149] refactor(FE): Refactor Card and SapronakClosingSkeleton components for readability --- .../closing/table/OverheadClosingTable.tsx | 98 +++++++++---------- .../IncomingSapronaksSummaryTable.tsx | 5 +- .../table/sapronak/IncomingSapronaksTable.tsx | 5 +- .../OutgoingSapronaksSummaryTable.tsx | 5 +- .../table/sapronak/OutgoingSapronaksTable.tsx | 5 +- 5 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index 152c588c..fef1edc6 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -258,58 +258,58 @@ const OverheadClosingTable = ({ /> )} {kandangId && !isLoadingOverhead && isResponseSuccess(overhead) && ( - -
-
-

Pembelian Kandang

+ +
+
+

Pembelian Kandang

+
+
+ +
+
+
+ Populasi Akhir KANDANG{' '} + Pemakaian + Di FARM
-
- -
-
-
- Populasi Akhir KANDANG{' '} - {' '} - Pemakaian Di FARM -
-
-
- Populasi Akhir Proyek -
-
-
- -
-
-
- {formatNumber(chickinPopulation ?? 0)} - - {formatCurrency( - isResponseSuccess(overhead) - ? overhead.data?.total.actual_total_amount - : 0 - )} -
-
-
- {formatNumber(generalInformation?.population ?? 0)} -
-
-
- -
-
-

- {formatNumber(kandangTotal || 0)} -

+
+
+ Populasi Akhir Proyek
- +
+ +
+
+
+ {formatNumber(chickinPopulation ?? 0)} + + {formatCurrency( + isResponseSuccess(overhead) + ? overhead.data?.total.actual_total_amount + : 0 + )} +
+
+
+ {formatNumber(generalInformation?.population ?? 0)} +
+
+
+ +
+
+

+ {formatNumber(kandangTotal || 0)} +

+
+
+ )} diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index 0e54a9de..d4e01bd2 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -133,7 +133,10 @@ const ClosingIncomingSapronaksSummaryTable = ({ >
{isLoadingIncomingSapronakSummaries ? ( - + ) : isResponseSuccess(incomingSapronakSummaries) && incomingSapronakSummaries.data.length === 0 ? ( {isLoadingIncomingSapronaks ? ( - + ) : isResponseSuccess(incomingSapronaks) && incomingSapronaks.data.length === 0 ? (
{isLoadingOutgoingSapronakSummaries ? ( - + ) : isResponseSuccess(outgoingSapronakSummaries) && outgoingSapronakSummaries.data.length === 0 ? ( {isLoadingOutgoingSapronaks ? ( - + ) : isResponseSuccess(outgoingSapronaks) && outgoingSapronaks.data.length === 0 ? ( Date: Thu, 19 Feb 2026 10:43:37 +0700 Subject: [PATCH 120/149] refactor(FE): Refactor table components to improve styling and structure --- .../closing/table/FinanceClosingTable.tsx | 63 +++++- .../table/HppExpeditionClosingTable.tsx | 93 ++++----- .../closing/table/OverheadClosingTable.tsx | 32 ++- .../pages/closing/table/SalesClosingTable.tsx | 88 +++++---- .../table/SapronakCalculationClosingTable.tsx | 70 ++++++- .../IncomingSapronaksSummaryTable.tsx | 165 +++++++--------- .../table/sapronak/IncomingSapronaksTable.tsx | 185 ++++++++--------- .../OutgoingSapronaksSummaryTable.tsx | 166 +++++++--------- .../table/sapronak/OutgoingSapronaksTable.tsx | 186 ++++++++---------- 9 files changed, 539 insertions(+), 509 deletions(-) diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx index de4d6e47..734e7a7b 100644 --- a/src/components/pages/closing/table/FinanceClosingTable.tsx +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -82,7 +82,7 @@ const FinanceClosingTable = ({ }, [finance]); return ( -
+
{isLoading ? ( ) : !isResponseSuccess(finance) ? ( @@ -96,10 +96,13 @@ const FinanceClosingTable = ({ -
+
Laba Rugi Brutto
@@ -127,10 +130,13 @@ const FinanceClosingTable = ({ variant='bordered' collapsible className={{ - wrapper: 'w-full', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', + title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} > -
+
data={hppTableData} isLoading={isLoading} @@ -263,6 +269,24 @@ const FinanceClosingTable = ({ ], }, ]} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} renderCustomRow={(row) => { const rowData = row.original; if (rowData.code === 'custom_row') { @@ -296,10 +320,13 @@ const FinanceClosingTable = ({ variant='bordered' collapsible className={{ - wrapper: 'w-full', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', + title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} > -
+
data={profitLossTableData} isLoading={isLoading} @@ -363,6 +390,25 @@ const FinanceClosingTable = ({ ), }, ]} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', + }} renderCustomRow={(row) => { const rowData = row.original; if (rowData.code === 'custom_row') { @@ -404,9 +450,6 @@ const FinanceClosingTable = ({ } return null; }} - className={{ - paginationClassName: 'hidden', - }} renderFooter={isResponseSuccess(finance)} />
diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx index a1ce4a94..20bd556d 100644 --- a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -91,53 +91,56 @@ const HppExpeditionClosingTable = ({ ); return ( - <> -
-
-

HPP Ekspedisi

- + + {isLoading ? ( + + ) : costOfRevenueExpeditionData.length === 0 ? ( + + ) : ( +
0} className={{ - wrapper: 'w-full bg-base-100', - body: 'p-0', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} - > - {isLoading ? ( - - ) : costOfRevenueExpeditionData.length === 0 ? ( - - ) : ( -
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - )} - - - - + /> + )} + + ); }; diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index fef1edc6..7ed62c0e 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -209,15 +209,18 @@ const OverheadClosingTable = ({ ); return ( - <> +
{isLoadingOverhead ? ( @@ -243,11 +246,24 @@ const OverheadClosingTable = ({ } columns={columns} className={{ - containerClassName: 'my-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: cn( - TABLE_DEFAULT_STYLING.headerColumnClassName, + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', 'whitespace-nowrap' ), + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} isLoading={isLoadingOverhead} renderFooter={ @@ -312,7 +328,7 @@ const OverheadClosingTable = ({ )} - +
); }; diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index fc3d4c55..e6ded656 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -316,50 +316,54 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { ); return ( - <> -
-
-

Penjualan

- + + {isLoading ? ( + + ) : salesData.length === 0 ? ( + + ) : ( +
0} className={{ - wrapper: 'w-full bg-base-100', - body: 'p-0', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} - > - {isLoading ? ( - - ) : salesData.length === 0 ? ( - - ) : ( -
0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - )} - - - - + /> + )} + + ); }; diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx index 1e2f7534..1ad4d3d7 100644 --- a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -179,7 +179,7 @@ const SapronakCalculationClosingTable = ({ ); return ( -
+
{/* Table DOC jika kategori Project Flock Growing */} {isLoading ? ( @@ -213,7 +216,22 @@ const SapronakCalculationClosingTable = ({ } columns={docColumns} className={{ - containerClassName: 'my-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} renderFooter={ isResponseSuccess(sapronakCalculation) && @@ -229,7 +247,10 @@ const SapronakCalculationClosingTable = ({ collapsible defaultCollapsed={true} className={{ - wrapper: 'w-full', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', + title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} > {isLoading ? ( @@ -251,7 +272,22 @@ const SapronakCalculationClosingTable = ({ } columns={ovkColumns} className={{ - containerClassName: 'my-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} renderFooter={ isResponseSuccess(sapronakCalculation) && @@ -267,7 +303,10 @@ const SapronakCalculationClosingTable = ({ collapsible defaultCollapsed={true} className={{ - wrapper: 'w-full', + wrapper: 'w-full rounded-lg border-none', + body: 'p-0', + title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white', + collapsible: 'rounded-lg', }} > {isLoading ? ( @@ -289,7 +328,22 @@ const SapronakCalculationClosingTable = ({ } columns={pakanColumns} className={{ - containerClassName: 'my-4', + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', }} renderFooter={ isResponseSuccess(sapronakCalculation) && diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index d4e01bd2..05fbebd2 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -5,12 +5,10 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatNumber } from '@/lib/helper'; +import { formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; @@ -56,8 +54,6 @@ const ClosingIncomingSapronaksSummaryTable = ({ } ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -94,97 +90,78 @@ const ClosingIncomingSapronaksSummaryTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries.data.length > 0 - : false - ); - } - }, [incomingSapronakSummaries, isResponseSuccess]); - return ( - - -
Ringkasan Sapronak Masuk
- - -
- } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
- {isLoadingIncomingSapronakSummaries ? ( - - ) : isResponseSuccess(incomingSapronakSummaries) && - incomingSapronakSummaries.data.length === 0 ? ( - - ) : ( - - data={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.data - : [] - } - columns={incomingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(incomingSapronakSummaries) - ? incomingSapronakSummaries?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingIncomingSapronakSummaries} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronakSummaries) && - incomingSapronakSummaries?.data?.length === 0, - }), - }} - /> - )} -
- -
+ {isLoadingIncomingSapronakSummaries ? ( + + ) : isResponseSuccess(incomingSapronakSummaries) && + incomingSapronakSummaries.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronakSummaries) + ? incomingSapronakSummaries?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronakSummaries} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + +
); }; diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index 268827b7..8e345441 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -5,13 +5,11 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { formatDate, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; @@ -52,8 +50,6 @@ const ClosingIncomingSapronaksTable = ({ ClosingApi.getAllIncomingSapronakFetcher ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -118,109 +114,90 @@ const ClosingIncomingSapronaksTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks.data.length > 0 - : false - ); - } - }, [incomingSapronaks, isResponseSuccess]); - return ( - - -
Sapronak Masuk
- - -
- } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
-
-
- -
+
+
+
- - {isLoadingIncomingSapronaks ? ( - - ) : isResponseSuccess(incomingSapronaks) && - incomingSapronaks.data.length === 0 ? ( - - ) : ( - - data={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.data - : [] - } - columns={incomingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(incomingSapronaks) - ? incomingSapronaks?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingIncomingSapronaks} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(incomingSapronaks) && - incomingSapronaks?.data?.length === 0, - }), - }} - /> - )}
- - + + {isLoadingIncomingSapronaks ? ( + + ) : isResponseSuccess(incomingSapronaks) && + incomingSapronaks.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + +
); }; diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index b5ce3f9b..e5c2eea5 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -5,12 +5,10 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatNumber } from '@/lib/helper'; +import { formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; @@ -56,8 +54,6 @@ const ClosingOutgoingSapronaksSummaryTable = ({ } ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -94,97 +90,79 @@ const ClosingOutgoingSapronaksSummaryTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries.data.length > 0 - : false - ); - } - }, [outgoingSapronakSummaries, isResponseSuccess]); - return ( - - -
Ringkasan Sapronak Keluar
- - -
- } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
- {isLoadingOutgoingSapronakSummaries ? ( - - ) : isResponseSuccess(outgoingSapronakSummaries) && - outgoingSapronakSummaries.data.length === 0 ? ( - - ) : ( - - data={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.data - : [] - } - columns={outgoingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(outgoingSapronakSummaries) - ? outgoingSapronakSummaries?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingOutgoingSapronakSummaries} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronakSummaries) && - outgoingSapronakSummaries?.data?.length === 0, - }), - }} - /> - )} -
- -
+ {isLoadingOutgoingSapronakSummaries ? ( + + ) : isResponseSuccess(outgoingSapronakSummaries) && + outgoingSapronakSummaries.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronakSummaries) + ? outgoingSapronakSummaries?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronakSummaries} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: + 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + +
); }; diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index dfb0178a..02ddc1bf 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -5,13 +5,11 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; -import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; -import Collapse from '@/components/Collapse'; -import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { formatDate, formatNumber } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ClosingApi } from '@/services/api/closing'; @@ -52,8 +50,6 @@ const ClosingOutgoingSapronaksTable = ({ ClosingApi.getAllOutgoingSapronakFetcher ); - const [open, setOpen] = useState(true); - const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); @@ -118,109 +114,91 @@ const ClosingOutgoingSapronaksTable = ({ } }, [sorting, updateFilter]); - useEffect(() => { - if (!open) { - setOpen( - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks.data.length > 0 - : false - ); - } - }, [outgoingSapronaks, isResponseSuccess]); - return ( - - -
Sapronak Keluar
- - -
- } - className='w-full!' - titleClassName='w-full p-0!' +
+ -
-
-
- -
+
+
+
- - {isLoadingOutgoingSapronaks ? ( - - ) : isResponseSuccess(outgoingSapronaks) && - outgoingSapronaks.data.length === 0 ? ( - - ) : ( - - data={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.data - : [] - } - columns={outgoingSapronaksColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(outgoingSapronaks) - ? outgoingSapronaks?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoadingOutgoingSapronaks} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-20': - isResponseSuccess(outgoingSapronaks) && - outgoingSapronaks?.data?.length === 0, - }), - }} - /> - )}
- - + + {isLoadingOutgoingSapronaks ? ( + + ) : isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks.data.length === 0 ? ( + + ) : ( + + data={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: 'w-full mb-0!', + tableWrapperClassName: + 'overflow-x-auto rounded-tr-none rounded-tl-none', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: + 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + +
); }; From 350ff0fbbea150a8211b3a14099707fa7f529813 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:46:03 +0700 Subject: [PATCH 121/149] refactor(FE): Simplify headerRowClassName formatting in tables --- .../closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx | 3 +-- .../pages/closing/table/sapronak/OutgoingSapronaksTable.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index e5c2eea5..60e4e310 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -150,8 +150,7 @@ const ClosingOutgoingSapronaksSummaryTable = ({ tableWrapperClassName: 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', - headerRowClassName: - 'border-b border-b-gray-200 bg-gray-50', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', bodyRowClassName: diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index 02ddc1bf..46f1a46f 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -186,8 +186,7 @@ const ClosingOutgoingSapronaksTable = ({ tableWrapperClassName: 'overflow-x-auto rounded-tr-none rounded-tl-none', tableClassName: 'w-full table-auto text-sm', - headerRowClassName: - 'border-b border-b-gray-200 bg-gray-50', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerColumnClassName: 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', bodyRowClassName: From 4c1f11d859db77fbfa4d7d44fd3a4c0d9d70cafa Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:50:52 +0700 Subject: [PATCH 122/149] refactor(FE): Update table column headers from '#' to 'No' --- .../closing/table/sapronak/IncomingSapronaksSummaryTable.tsx | 2 +- .../pages/closing/table/sapronak/IncomingSapronaksTable.tsx | 2 +- .../closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx | 2 +- .../pages/closing/table/sapronak/OutgoingSapronaksTable.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index 05fbebd2..ca16d143 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -60,7 +60,7 @@ const ClosingIncomingSapronaksSummaryTable = ({ const incomingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index 8e345441..c8f225d9 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -55,7 +55,7 @@ const ClosingIncomingSapronaksTable = ({ const incomingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index 60e4e310..e1c41b30 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -60,7 +60,7 @@ const ClosingOutgoingSapronaksSummaryTable = ({ const outgoingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index 46f1a46f..d2179fb3 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -55,7 +55,7 @@ const ClosingOutgoingSapronaksTable = ({ const outgoingSapronaksColumns: ColumnDef[] = [ { - header: '#', + header: 'No', cell: (props) => props.row.index + 1, }, { From c3a69bc66aa465be6689f246b930555f68c45362 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 10:55:07 +0700 Subject: [PATCH 123/149] refactor(FE): Add Icon component and update table styles --- .../pages/closing/table/FinanceClosingTable.tsx | 3 ++- .../sapronak/IncomingSapronaksSummaryTable.tsx | 1 + .../table/sapronak/IncomingSapronaksTable.tsx | 15 ++++++++++++++- .../sapronak/OutgoingSapronaksSummaryTable.tsx | 1 + .../table/sapronak/OutgoingSapronaksTable.tsx | 15 ++++++++++++++- 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx index 734e7a7b..1e1195fe 100644 --- a/src/components/pages/closing/table/FinanceClosingTable.tsx +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -151,7 +151,7 @@ const FinanceClosingTable = ({ .filter((row) => row.code !== 'custom_row').length; return dataRowsBefore + 1; }, - footer: (props) => { + footer: () => { return 'HPP'; }, }, @@ -286,6 +286,7 @@ const FinanceClosingTable = ({ footerRowClassName: 'border-t-2 border-gray-300', footerColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + paginationClassName: 'hidden', }} renderCustomRow={(row) => { const rowData = row.original; diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx index ca16d143..9c43675c 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksSummaryTable.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; diff --git a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx index c8f225d9..0bbad454 100644 --- a/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/IncomingSapronaksTable.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; @@ -135,7 +136,19 @@ const ClosingIncomingSapronaksTable = ({ placeholder='Cari Sapronak Masuk' value={tableFilterState.search} onChange={searchChangeHandler} - className={{ wrapper: 'sm:max-w-3xs' }} + startAdornment={ + + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} />
diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx index e1c41b30..591b38d6 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksSummaryTable.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import Card from '@/components/Card'; diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index d2179fb3..652b3f60 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; +import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Card from '@/components/Card'; @@ -135,7 +136,19 @@ const ClosingOutgoingSapronaksTable = ({ placeholder='Cari Sapronak Keluar' value={tableFilterState.search} onChange={searchChangeHandler} - className={{ wrapper: 'sm:max-w-3xs' }} + startAdornment={ + + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} /> From 8a1e0f080f0cf0183f92275ea29d1b622da47394 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 11:14:39 +0700 Subject: [PATCH 124/149] refactor(FE): Refactor table components for consistent styling and cleanup --- .../table/SapronakCalculationClosingTable.tsx | 2 +- .../sapronak/IncomingSapronaksSummaryTable.tsx | 18 ++++++------------ .../table/sapronak/IncomingSapronaksTable.tsx | 13 ++++++------- .../sapronak/OutgoingSapronaksSummaryTable.tsx | 16 +++++----------- .../table/sapronak/OutgoingSapronaksTable.tsx | 13 ++++++------- 5 files changed, 24 insertions(+), 38 deletions(-) diff --git a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx index 1ad4d3d7..0f0a7857 100644 --- a/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx +++ b/src/components/pages/closing/table/SapronakCalculationClosingTable.tsx @@ -179,7 +179,7 @@ const SapronakCalculationClosingTable = ({ ); return ( -
+
{/* Table DOC jika kategori Project Flock Growing */} = (e) => { - updateFilter('search', e.target.value); - }; - // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -92,10 +87,10 @@ const ClosingIncomingSapronaksSummaryTable = ({ }, [sorting, updateFilter]); return ( -
+
+
-
+
= (e) => { - updateFilter('search', e.target.value); - }; - // track sorting useEffect(() => { const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); @@ -92,7 +87,7 @@ const ClosingOutgoingSapronaksSummaryTable = ({ }, [sorting, updateFilter]); return ( -
+
+
-
+
Date: Thu, 19 Feb 2026 11:27:24 +0700 Subject: [PATCH 125/149] refactor(FE): Refactor table and tab styles for consistent spacing and layout --- src/components/pages/closing/ClosingDetailTabs.tsx | 2 +- src/components/pages/closing/ClosingsTable.tsx | 2 +- src/components/pages/closing/table/FinanceClosingTable.tsx | 2 +- .../pages/closing/table/HppExpeditionClosingTable.tsx | 2 +- src/components/pages/closing/table/OverheadClosingTable.tsx | 2 +- src/components/pages/closing/table/SalesClosingTable.tsx | 2 +- .../pages/closing/table/SapronakCalculationClosingTable.tsx | 2 +- .../pages/closing/table/sapronak/IncomingSapronaksTable.tsx | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index 59fea1ba..d5dc996c 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -126,7 +126,7 @@ const ClosingDetail: React.FC = ({ variant='boxed' className={{ tabHeaderWrapper: - 'justify-between items-center py-3 border-b border-base-content/10', + 'relative justify-between items-center py-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10', tab: 'w-fit', content: 'p-0 m-0', }} diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 2716932f..5a8aab17 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -17,7 +17,7 @@ import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; -import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { LocationApi } from '@/services/api/master-data'; diff --git a/src/components/pages/closing/table/FinanceClosingTable.tsx b/src/components/pages/closing/table/FinanceClosingTable.tsx index 1e1195fe..82e8b309 100644 --- a/src/components/pages/closing/table/FinanceClosingTable.tsx +++ b/src/components/pages/closing/table/FinanceClosingTable.tsx @@ -82,7 +82,7 @@ const FinanceClosingTable = ({ }, [finance]); return ( -
+
{isLoading ? ( ) : !isResponseSuccess(finance) ? ( diff --git a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx index 20bd556d..5389e3d5 100644 --- a/src/components/pages/closing/table/HppExpeditionClosingTable.tsx +++ b/src/components/pages/closing/table/HppExpeditionClosingTable.tsx @@ -91,7 +91,7 @@ const HppExpeditionClosingTable = ({ ); return ( -
+
+
{ ); return ( -
+
+
{/* Table DOC jika kategori Project Flock Growing */} +
Date: Thu, 19 Feb 2026 11:38:34 +0700 Subject: [PATCH 126/149] refactor(FE): Refactor UI and improve conditional rendering in closing pages --- .../pages/closing/ClosingKandangList.tsx | 7 +++---- .../closing/table/OverheadClosingTable.tsx | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/pages/closing/ClosingKandangList.tsx b/src/components/pages/closing/ClosingKandangList.tsx index dd3083a7..b324c512 100644 --- a/src/components/pages/closing/ClosingKandangList.tsx +++ b/src/components/pages/closing/ClosingKandangList.tsx @@ -10,18 +10,17 @@ const ClosingKandangList = ({ projectData?: ProjectFlock; }) => { return ( -
+
-

Kandang

-
+
{projectData?.kandangs?.map((kandang) => ( diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index 2d3faaec..a6c31e6c 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -224,15 +224,28 @@ const OverheadClosingTable = ({ > {isLoadingOverhead ? ( - ) : !isResponseSuccess(overhead) || - (!kandangId && overhead.data?.overheads.length === 0) || - (kandangId && !isResponseSuccess(overheadKandang)) ? ( + ) : !isResponseSuccess(overhead) ? ( + ) : kandangId && !isResponseSuccess(overheadKandang) ? ( + + ) : (!kandangId && overhead.data?.overheads.length === 0) || + (kandangId && + isResponseSuccess(overheadKandang) && + overheadKandang.data?.overheads.length === 0) ? ( + ) : ( data={ From 82975219a89773067f3d0d852238c7f1e8bd5ec7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 11:43:46 +0700 Subject: [PATCH 127/149] refactor(FE): Update border styles for ClosingDetailTabs and ClosingKandangList --- src/components/pages/closing/ClosingDetailTabs.tsx | 2 +- src/components/pages/closing/ClosingKandangList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/closing/ClosingDetailTabs.tsx b/src/components/pages/closing/ClosingDetailTabs.tsx index d5dc996c..dc8bd6f8 100644 --- a/src/components/pages/closing/ClosingDetailTabs.tsx +++ b/src/components/pages/closing/ClosingDetailTabs.tsx @@ -126,7 +126,7 @@ const ClosingDetail: React.FC = ({ variant='boxed' className={{ tabHeaderWrapper: - 'relative justify-between items-center py-3 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10', + 'relative justify-between items-center py-3 before:absolute before:top-0 before:left-0 before:right-0 before:-mx-4 before:border-t before:border-base-content/10 after:absolute after:bottom-0 after:left-0 after:right-0 after:-mx-4 after:border-b after:border-base-content/10', tab: 'w-fit', content: 'p-0 m-0', }} diff --git a/src/components/pages/closing/ClosingKandangList.tsx b/src/components/pages/closing/ClosingKandangList.tsx index b324c512..bd2823c4 100644 --- a/src/components/pages/closing/ClosingKandangList.tsx +++ b/src/components/pages/closing/ClosingKandangList.tsx @@ -10,7 +10,7 @@ const ClosingKandangList = ({ projectData?: ProjectFlock; }) => { return ( -
+
From a0af934002aef3d0b6a4c0554abbc826b709871d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 11:52:57 +0700 Subject: [PATCH 128/149] feat(FE): Add headings and improve layout for financial tables --- .../pages/closing/ClosingKandangList.tsx | 1 + .../closing/table/FinanceClosingTable.tsx | 99 +++++++++++++------ 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/src/components/pages/closing/ClosingKandangList.tsx b/src/components/pages/closing/ClosingKandangList.tsx index bd2823c4..4ecf607f 100644 --- a/src/components/pages/closing/ClosingKandangList.tsx +++ b/src/components/pages/closing/ClosingKandangList.tsx @@ -14,6 +14,7 @@ const ClosingKandangList = ({
+

Kandang

{projectData?.kandangs?.map((kandang) => (
- - data={isResponseSuccess(closings) ? closings?.data : []} - columns={closingsColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={isResponseSuccess(closings) ? closings?.meta?.page : 0} - totalItems={ - isResponseSuccess(closings) ? closings?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoadingClosings} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-0': - isResponseSuccess(closings) && closings?.data?.length === 0, - }), - }} - /> + {isLoadingClosings ? ( +
+ +
+ ) : data.length === 0 ? ( + + } + title='Data Closing Belum Tersedia' + subtitle='Tidak ada data closing untuk saat ini.' + /> + ) : ( + + data={data} + columns={closingsColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={tableFilterState.page} + totalItems={ + isResponseSuccess(closings) ? closings?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingClosings} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-0': data.length === 0, + }), + }} + /> + )}
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ { + if (!Array.isArray(val)) { + formik.setFieldValue( + 'location_id', + val?.value ? String(val.value) : null + ); + } + }} + onInputChange={setLocationInputValue} + isLoading={isLoadingLocationOptions} + isClearable + onMenuScrollToBottom={loadMoreLocations} + className={{ wrapper: 'w-full' }} + /> + + { + if (!Array.isArray(val)) { + formik.setFieldValue('project_status', val?.value || null); + } + }} + className={{ wrapper: 'w-full' }} + isClearable={true} + /> +
+ + {/* Modal Footer */} +
+ + +
+ +
); }; diff --git a/src/components/pages/closing/filter/ClosingFilter.ts b/src/components/pages/closing/filter/ClosingFilter.ts new file mode 100644 index 00000000..77f0c9d2 --- /dev/null +++ b/src/components/pages/closing/filter/ClosingFilter.ts @@ -0,0 +1,13 @@ +import * as yup from 'yup'; + +export type ClosingFilterType = { + location_id: string | null; + project_status: string | null; +}; + +export const ClosingFilterSchema = yup.object({ + location_id: yup.string().nullable(), + project_status: yup.string().nullable(), +}); + +export type ClosingFilterValues = yup.InferType; diff --git a/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx new file mode 100644 index 00000000..4b59510a --- /dev/null +++ b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Closing } from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; + +const ClosingTableSkeleton = ({ + columns, + icon, + title, + subtitle, +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title: string; + subtitle: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ClosingTableSkeleton; From e9784bd5ed2a9964f82cc868481e4e97f373b59e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 14:33:35 +0700 Subject: [PATCH 134/149] refactor(FE): Refactor ClosingsTable component and update styles --- src/app/closing/page.tsx | 2 +- .../pages/closing/ClosingsTable.tsx | 216 +++++++++++------- 2 files changed, 133 insertions(+), 85 deletions(-) diff --git a/src/app/closing/page.tsx b/src/app/closing/page.tsx index acaa3ee8..0717c350 100644 --- a/src/app/closing/page.tsx +++ b/src/app/closing/page.tsx @@ -2,7 +2,7 @@ import ClosingsTable from '@/components/pages/closing/ClosingsTable'; const Closing = () => { return ( -
+
); diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 294106ff..ad402829 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -3,15 +3,15 @@ import { ChangeEventHandler, useEffect, useState, useMemo } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import Button from '@/components/Button'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; import Modal, { useModal } from '@/components/Modal'; import SelectInputRadio from '@/components/input/SelectInputRadio'; @@ -31,32 +31,66 @@ import { import ClosingTableSkeleton from '@/components/pages/closing/skeleton/ClosingTableSkeleton'; const RowOptionsMenu = ({ - type = 'dropdown', props, + popoverPosition = 'bottom', + detailClickHandler, }: { - type: 'dropdown' | 'collapse'; props: CellContext; + popoverPosition: 'bottom' | 'top'; + detailClickHandler: (id: number) => void; }) => { + const popoverId = `closing#${props.row.original.id}`; + const popoverAnchorName = `--anchor-closing#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + + const detailClickHandlerWrapper = () => { + detailClickHandler(props.row.original.id); + closePopover(); + }; + return ( - -
- - - -
-
+
+ + + + + +
+ + + +
+
+
); }; const ClosingsTable = () => { + // ===== ROUTER ===== + const router = useRouter(); + // ===== FILTER MODAL STATE ===== const filterModal = useModal(); @@ -170,22 +204,18 @@ const ClosingsTable = () => { const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + const detailClickHandler = (id: number) => { + router.push(`/closing/detail/?closingId=${id}`); + }; return ( - <> - {currentPageSize > 3 && ( - - - - )} - - {currentPageSize <= 3 && ( - - - - )} - + ); }, }, @@ -268,16 +298,32 @@ const ClosingsTable = () => { return ( <> -
-
-
-
+
+
+
+
+ {/* Space for action buttons if needed in the future */} +
+ +
+ } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} />
-
- {isLoadingClosings ? ( -
- -
- ) : data.length === 0 ? ( - - } - title='Data Closing Belum Tersedia' - subtitle='Tidak ada data closing untuk saat ini.' - /> - ) : ( - - data={data} - columns={closingsColumns} - pageSize={tableFilterState.pageSize} - onPageSizeChange={setPageSize} - rowOptions={[10, 20, 50, 100]} - page={tableFilterState.page} - totalItems={ - isResponseSuccess(closings) ? closings?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoadingClosings} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn({ - 'w-full mb-0': data.length === 0, - }), - }} - /> - )} + {isLoadingClosings ? ( +
+ +
+ ) : data.length === 0 ? ( + + } + title='Data Closing Belum Tersedia' + subtitle='Tidak ada data closing untuk saat ini.' + /> + ) : ( + + data={isResponseSuccess(closings) ? closings?.data : []} + columns={closingsColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={isResponseSuccess(closings) ? closings?.meta?.page : 0} + totalItems={ + isResponseSuccess(closings) ? closings?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingClosings} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn('mt-3', { + 'w-full mb-0': + isResponseSuccess(closings) && closings?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
{/* Filter Modal */} From 60e360537e92e2074c21851823b0ccd078a1929a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 14:37:20 +0700 Subject: [PATCH 135/149] refactor(FE): Refactor ClosingsTable header for improved styling --- src/components/pages/closing/ClosingsTable.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index ad402829..22b4dd52 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -300,11 +300,7 @@ const ClosingsTable = () => { <>
-
-
- {/* Space for action buttons if needed in the future */} -
- +
Date: Thu, 19 Feb 2026 14:41:21 +0700 Subject: [PATCH 136/149] feat(FE): Add StatusBadge to display project status in ClosingsTable --- .../pages/closing/ClosingsTable.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 22b4dd52..12114110 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -13,6 +13,7 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; +import StatusBadge from '@/components/helper/StatusBadge'; import Modal, { useModal } from '@/components/Modal'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import { useFormik } from 'formik'; @@ -24,6 +25,7 @@ import { LocationApi } from '@/services/api/master-data'; import { Location } from '@/types/api/master-data/location'; import { ClosingApi } from '@/services/api/closing'; import { Closing } from '@/types/api/closing'; +import { Color } from '@/types/theme'; import { ClosingFilterSchema, ClosingFilterType, @@ -91,6 +93,21 @@ const ClosingsTable = () => { // ===== ROUTER ===== const router = useRouter(); + // ===== STATUS BADGE COLOR HELPER ===== + const getProjectStatusBadgeColor = (status: string): Color => { + const normalizedValue = status.toLowerCase(); + + if (normalizedValue === 'aktif') { + return 'success'; + } + + if (normalizedValue === 'pengajuan') { + return 'neutral'; + } + + return 'neutral'; + }; + // ===== FILTER MODAL STATE ===== const filterModal = useModal(); @@ -195,6 +212,19 @@ const ClosingsTable = () => { { accessorKey: 'project_status', header: 'Status', + cell: (props) => { + const status = props.row.original.project_status; + const badgeColor = getProjectStatusBadgeColor(status); + return ( + + ); + }, }, { header: 'Aksi', From a4ff92520ab12020e9ccbc242aa1a82a93e535e8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 14:50:43 +0700 Subject: [PATCH 137/149] refactor(FE): Update toast message for Project Flock approval/rejection --- .../pages/production/project-flock/ProjectFlockTable.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index e8280fa8..8ec79009 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -271,7 +271,11 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { ); if (isResponseSuccess(approveProjectFlockRes)) { - toast.success('Project Flock berhasil di-approve!'); + const successMessage = + approvalAction === 'APPROVED' + ? 'Project Flock berhasil di-approve!' + : 'Project Flock berhasil di-reject!'; + toast.success(successMessage); confirmModal.closeModal(); } if (isResponseError(approveProjectFlockRes)) { From 6ac903313c766c9797563d17b1fb198c86ac6fe9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 15:22:28 +0700 Subject: [PATCH 138/149] refactor(FE): Refactor chickin approval modal logic into Zustand store --- .../chickin/form/tabs/ChickLogsView.tsx | 62 ++++++------------- .../project-flock/ProjectFlockTable.tsx | 51 +++++++++++++++ .../production/chickin/chickin.store.ts | 19 ++++++ .../chickin/slices/chickin-approval.slice.ts | 58 +++++++++++++++++ 4 files changed, 147 insertions(+), 43 deletions(-) create mode 100644 src/stores/production/chickin/chickin.store.ts create mode 100644 src/stores/production/chickin/slices/chickin-approval.slice.ts diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index e800ee68..bdffda33 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -2,8 +2,6 @@ import Alert from '@/components/Alert'; import Button from '@/components/Button'; import Card from '@/components/Card'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useModal } from '@/components/Modal'; -import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import PillBadge from '@/components/PillBadge'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; @@ -13,6 +11,7 @@ import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandan import { Icon } from '@iconify/react'; import { useState } from 'react'; import toast from 'react-hot-toast'; +import { useChickinStore } from '@/stores/production/chickin/chickin.store'; const ChickinLogsView = ({ initialValues, @@ -23,32 +22,26 @@ const ChickinLogsView = ({ afterSubmit?: () => void; rawDataApprovals: BaseApproval[]; }) => { - const confirmModal = useModal(); - const [isApproveLoading, setIsApproveLoading] = useState(false); const [chickinErrorMessage, setChickinErrorMessage] = useState(''); + const { openChickinApproveModal } = useChickinStore(); const handleClickApprove = () => { - confirmModal.openModal(); - }; - - const confirmationModalApproveClickHandler = async (notes?: string) => { - setChickinErrorMessage(''); - setIsApproveLoading(true); - const approveChickinRes = await ChickinApi.singleApproval( - initialValues?.id as number, - 'APPROVED', - notes - ); - if (isResponseSuccess(approveChickinRes)) { - toast.success(approveChickinRes?.message as string); - } - if (isResponseError(approveChickinRes)) { - toast.error(approveChickinRes?.message as string); - setChickinErrorMessage(approveChickinRes?.message as string); - } - confirmModal.closeModal(); - setIsApproveLoading(false); - afterSubmit && afterSubmit(); + openChickinApproveModal(initialValues, async (notes?: string) => { + setChickinErrorMessage(''); + const approveChickinRes = await ChickinApi.singleApproval( + initialValues?.id as number, + 'APPROVED', + notes + ); + if (isResponseSuccess(approveChickinRes)) { + toast.success(approveChickinRes?.message as string); + } + if (isResponseError(approveChickinRes)) { + toast.error(approveChickinRes?.message as string); + setChickinErrorMessage(approveChickinRes?.message as string); + } + afterSubmit && afterSubmit(); + }); }; return ( @@ -83,7 +76,7 @@ const ChickinLogsView = ({ key={chickin.id || index} variant='bordered' className={{ - wrapper: 'w-full', + wrapper: 'w-full mt-3', body: 'p-3', }} > @@ -176,23 +169,6 @@ const ChickinLogsView = ({
)}
- - { - confirmationModalApproveClickHandler(notes); - }, - isLoading: isApproveLoading, - }} - /> ); }; diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 8ec79009..040948ff 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -36,6 +36,7 @@ import PopoverContent from '@/components/popover/PopoverContent'; import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal'; import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; +import { useChickinStore } from '@/stores/production/chickin/chickin.store'; const RowOptionsMenu = ({ props, @@ -193,6 +194,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const deleteModal = useModal(); const confirmModal = useModal(); const successModal = useModal(); + const chickinApproveModal = useModal(); const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' ); @@ -200,6 +202,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const [isApproveLoading, setIsApproveLoading] = useState(false); const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); + const { + isChickinApproveModalOpen, + isChickinApproveLoading, + chickinApproveCallback, + closeChickinApproveModal, + setChickinApproveLoading, + } = useChickinStore(); // ===== Fetch Data ===== const { @@ -292,6 +301,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { refreshProjectFlocks(); }, [refresh]); + useEffect(() => { + if (isChickinApproveModalOpen) { + chickinApproveModal.openModal(); + } else { + chickinApproveModal.closeModal(); + } + }, [isChickinApproveModalOpen, chickinApproveModal]); + useEffect(() => { if (isSuccess) { successModal.openModal(); @@ -974,6 +991,40 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { onClose={handleSuccessModalClose} secondaryButton={undefined} /> + + {/* Chickin Approval Modal */} + { + closeChickinApproveModal(); + chickinApproveModal.closeModal(); + }, + }} + primaryButton={{ + text: 'Ya', + color: 'success', + onClick: async (notes) => { + if (chickinApproveCallback) { + setChickinApproveLoading(true); + try { + await chickinApproveCallback(notes); + } finally { + setChickinApproveLoading(false); + closeChickinApproveModal(); + chickinApproveModal.closeModal(); + } + } + }, + isLoading: isChickinApproveLoading, + }} + /> ); }; diff --git a/src/stores/production/chickin/chickin.store.ts b/src/stores/production/chickin/chickin.store.ts new file mode 100644 index 00000000..697b1de4 --- /dev/null +++ b/src/stores/production/chickin/chickin.store.ts @@ -0,0 +1,19 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; +import { ChickinApprovalSlice } from '@/stores/production/chickin/slices/chickin-approval.slice'; + +export type ChickinStore = ChickinApprovalSlice; + +export const useChickinStore = create()( + devtools( + (...args) => ({ + ...createChickinApprovalSlice(...args), + }), + { + name: 'ChickinStore', + } + ) +); diff --git a/src/stores/production/chickin/slices/chickin-approval.slice.ts b/src/stores/production/chickin/slices/chickin-approval.slice.ts new file mode 100644 index 00000000..30f0a857 --- /dev/null +++ b/src/stores/production/chickin/slices/chickin-approval.slice.ts @@ -0,0 +1,58 @@ +import { StateCreator } from 'zustand'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; + +export type ChickinApprovalSlice = { + // State + isChickinApproveModalOpen: boolean; + selectedChickinForApproval: ProjectFlockKandang | null; + isChickinApproveLoading: boolean; + chickinApproveCallback: ((notes?: string) => Promise) | null; + + // Actions + openChickinApproveModal: ( + data: ProjectFlockKandang, + callback: (notes?: string) => Promise + ) => void; + closeChickinApproveModal: () => void; + setChickinApproveLoading: (loading: boolean) => void; + resetChickinApproval: () => void; +}; + +export const createChickinApprovalSlice: StateCreator< + ChickinApprovalSlice, + [], + [], + ChickinApprovalSlice +> = (set) => ({ + // Initial state + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + isChickinApproveLoading: false, + chickinApproveCallback: null, + + // Actions + openChickinApproveModal: (data, callback) => + set({ + isChickinApproveModalOpen: true, + selectedChickinForApproval: data, + chickinApproveCallback: callback, + }), + + closeChickinApproveModal: () => + set({ + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + chickinApproveCallback: null, + }), + + setChickinApproveLoading: (loading) => + set({ isChickinApproveLoading: loading }), + + resetChickinApproval: () => + set({ + isChickinApproveModalOpen: false, + selectedChickinForApproval: null, + isChickinApproveLoading: false, + chickinApproveCallback: null, + }), +}); From e22f95cc58fe77ba5666f5baf972b34e6af69f10 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 15:28:27 +0700 Subject: [PATCH 139/149] refactor(FE): Remove unused variable `approval.step_name` in RecordingTable --- src/components/pages/production/recording/RecordingTable.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index a67f44f9..13068563 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -852,8 +852,7 @@ const RecordingTable = () => { const status = approval.action; const statusColor = getStatusBadgeColor(status); - - const statusText = approval.step_name || getStatusText(status); + const statusText = getStatusText(status); return ( Date: Thu, 19 Feb 2026 15:35:08 +0700 Subject: [PATCH 140/149] refactor(FE): Update status mappings for "CREATED" to "Pengajuan" --- .../pages/production/recording/RecordingTable.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 13068563..29f2a82a 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -41,7 +41,8 @@ const statusTextMap: Record = { Disetujui: 'Disetujui', REJECTED: 'Ditolak', Ditolak: 'Ditolak', - CREATED: 'Dibuat', + CREATED: 'Pengajuan', + Pengajuan: 'Pengajuan', UPDATED: 'Diperbarui', }; @@ -59,13 +60,11 @@ const statusBadgeColorMap: Record = { rejected: 'error', ditolak: 'error', CREATED: 'neutral', - Dibuat: 'neutral', + Pengajuan: 'neutral', created: 'neutral', - dibuat: 'neutral', + pengajuan: 'neutral', UPDATED: 'warning', - Diperbarui: 'warning', updated: 'warning', - diperbarui: 'warning', }; const getStatusBadgeColor = (status: string): Color => { From 1a137e7500d638fcb4bb786b53dbf5194a8cbc4e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 19 Feb 2026 15:37:46 +0700 Subject: [PATCH 141/149] refactor(FE): Normalize status keys to uppercase in status utilities --- .../production/recording/RecordingTable.tsx | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 29f2a82a..65e658f9 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -38,37 +38,26 @@ import { Color } from '@/types/theme'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { APPROVED: 'Disetujui', - Disetujui: 'Disetujui', REJECTED: 'Ditolak', - Ditolak: 'Ditolak', CREATED: 'Pengajuan', - Pengajuan: 'Pengajuan', UPDATED: 'Diperbarui', }; const getStatusText = (status: string): string => { - return statusTextMap[status] || status; + const normalizedStatus = status.toUpperCase(); + return statusTextMap[normalizedStatus] || status; }; const statusBadgeColorMap: Record = { APPROVED: 'success', - Disetujui: 'success', - approved: 'success', - disetujui: 'success', REJECTED: 'error', - Ditolak: 'error', - rejected: 'error', - ditolak: 'error', CREATED: 'neutral', - Pengajuan: 'neutral', - created: 'neutral', - pengajuan: 'neutral', UPDATED: 'warning', - updated: 'warning', }; const getStatusBadgeColor = (status: string): Color => { - return statusBadgeColorMap[status] || 'neutral'; + const normalizedStatus = status.toUpperCase(); + return statusBadgeColorMap[normalizedStatus] || 'neutral'; }; const RowOptionsMenu = ({ From bbbd767cf2ae63d8318dde05002db6f9c7d5c965 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 20 Feb 2026 09:21:42 +0700 Subject: [PATCH 142/149] refactor(FE): Fix week calculation logic in UniformityForm --- .../uniformity/form/UniformityForm.tsx | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 33b649c4..724f7b81 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -203,16 +203,19 @@ const UniformityForm = ({ // ===== 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, - RecordingApi.getAllFetcher + recordingsUrl ? RecordingApi.getAllFetcher : null ); // ===== FORM CONFIGURATION ===== @@ -400,50 +403,46 @@ const UniformityForm = ({ useEffect(() => { if ( projectFlockKandangLookup?.chick_in_date && - projectFlockKandangLookup?.project_flock_kandang_id && - isResponseSuccess(recordingsData) && - recordingsData.data + projectFlockKandangLookup?.project_flock_kandang_id ) { - const matchingRecordings = recordingsData.data.filter( - (recording: Recording) => - recording.project_flock?.project_flock_kandang_id === - projectFlockKandangLookup.project_flock_kandang_id - ); + const chickInDate = new Date(projectFlockKandangLookup.chick_in_date); + chickInDate.setHours(0, 0, 0, 0); - matchingRecordings.sort( - (a: Recording, b: Recording) => - new Date(a.record_datetime).getTime() - - new Date(b.record_datetime).getTime() - ); + let initialWeek = 18; - const earliestRecording = matchingRecordings[0]; + 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() + ); - if (earliestRecording) { - const chickInDate = new Date(projectFlockKandangLookup.chick_in_date); - chickInDate.setHours(0, 0, 0, 0); - - const earliestRecordDate = new Date(earliestRecording.record_datetime); - earliestRecordDate.setHours(0, 0, 0, 0); - - const initialWeek = - earliestRecording.project_flock?.production_standart?.week || 18; - - 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); - - formik.setFieldValue('week', initialWeek + weeksDiff); - } else { - formik.setFieldValue('week', initialWeek); + 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); + + formik.setFieldValue('week', initialWeek + weeksDiff); + } else { + formik.setFieldValue('week', initialWeek); + } } }, [ projectFlockKandangLookup?.chick_in_date, From b35b6c2ab8fd536d517fb32c6ad8da3de38b8c35 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 20 Feb 2026 10:11:16 +0700 Subject: [PATCH 143/149] feat(FE): Add submittedActionType state to track modal action type --- .../TransferToLayingFormModal.tsx | 17 ++++++++++++++--- .../TransferToLayingsTable.tsx | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx index 78de70e1..cf80be89 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx @@ -83,6 +83,9 @@ const TransferToLayingFormModal = () => { TransferToLayingFormValues | undefined >(undefined); const [formErrorMessage, setFormErrorMessage] = useState(null); + const [submittedActionType, setSubmittedActionType] = useState< + 'add' | 'edit' | null + >(null); // Flock Source const { @@ -203,6 +206,7 @@ const TransferToLayingFormModal = () => { }; setFormikLastValues(values); + setSubmittedActionType(modalAction as 'add' | 'edit'); switch (modalAction) { case 'add': @@ -1059,10 +1063,17 @@ const TransferToLayingFormModal = () => { setFormikLastValues(undefined)} + onClose={() => { + setFormikLastValues(undefined); + setSubmittedActionType(null); + }} secondaryButton={undefined} /> diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index 438c529c..bf4c31e3 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -680,6 +680,7 @@ const TransferToLayingsTable = () => { subtitleText='Are you sure you want to delete this data? ' transferToLayingIds={selectedRowIds} primaryButton={{ + text: 'Delete', isLoading: isDeleteLoading, color: 'error', onClick: confirmationModalDeleteClickHandler, @@ -704,6 +705,7 @@ const TransferToLayingsTable = () => { withNote noteLabel='Notes Approval' primaryButton={{ + text: 'Approve', isLoading: isApproveLoading, onClick: confirmationModalApproveClickHandler, }} @@ -735,6 +737,7 @@ const TransferToLayingsTable = () => { }, }} primaryButton={{ + text: 'Reject', isLoading: isRejectLoading, color: 'error', onClick: confirmationModalRejectClickHandler, From 4c3e7c615fc80abde3c566707b1a68a183e47444 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 20 Feb 2026 10:12:06 +0700 Subject: [PATCH 144/149] refactor(FE): Refactor conditional text formatting in modal component --- .../transfer-to-laying/TransferToLayingFormModal.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx index cf80be89..84e67a5b 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingFormModal.tsx @@ -1063,7 +1063,11 @@ const TransferToLayingFormModal = () => { Date: Fri, 20 Feb 2026 10:23:47 +0700 Subject: [PATCH 145/149] feat(FE): Add project flock closing modal and zustand store --- .../project-flock/ProjectFlockTable.tsx | 58 ++++++++++++++ .../closing/ProjectFlockClosingForm.tsx | 78 ++++++++----------- .../project-flock-closing.store.ts | 19 +++++ .../slices/project-flock-closing.slice.ts | 70 +++++++++++++++++ 4 files changed, 178 insertions(+), 47 deletions(-) create mode 100644 src/stores/production/project-flock-closing/project-flock-closing.store.ts create mode 100644 src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 040948ff..040771a8 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -37,6 +37,7 @@ import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal'; import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; import { useChickinStore } from '@/stores/production/chickin/chickin.store'; +import { useProjectFlockClosingStore } from '@/stores/production/project-flock-closing/project-flock-closing.store'; const RowOptionsMenu = ({ props, @@ -195,6 +196,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { const confirmModal = useModal(); const successModal = useModal(); const chickinApproveModal = useModal(); + const closingModal = useModal(); const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' ); @@ -210,6 +212,15 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { setChickinApproveLoading, } = useChickinStore(); + const { + isClosingModalOpen, + isKandangClosed, + isClosingLoading, + closingCallback, + closeClosingModal, + setClosingLoading, + } = useProjectFlockClosingStore(); + // ===== Fetch Data ===== const { data: projectFlocks, @@ -309,6 +320,14 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { } }, [isChickinApproveModalOpen, chickinApproveModal]); + useEffect(() => { + if (isClosingModalOpen) { + closingModal.openModal(); + } else { + closingModal.closeModal(); + } + }, [isClosingModalOpen, closingModal]); + useEffect(() => { if (isSuccess) { successModal.openModal(); @@ -1025,6 +1044,45 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { isLoading: isChickinApproveLoading, }} /> + + {/* Project Flock Closing Modal */} + { + closeClosingModal(); + closingModal.closeModal(); + }, + }} + primaryButton={{ + text: 'Ya', + color: 'error', + isLoading: isClosingLoading, + onClick: async () => { + if (closingCallback) { + setClosingLoading(true); + try { + await closingCallback(!isKandangClosed ? 'close' : 'unclose'); + } finally { + setClosingLoading(false); + closeClosingModal(); + closingModal.closeModal(); + refreshProjectFlocks(); + } + } + }, + }} + /> ); }; diff --git a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx index f963a793..e73a157a 100644 --- a/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx +++ b/src/components/pages/production/project-flock/closing/ProjectFlockClosingForm.tsx @@ -17,9 +17,8 @@ import { Icon } from '@iconify/react'; import useSWR from 'swr'; import { ProjectFlockKandangApi } from '@/services/api/production/project-flock-kandang'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { useModal } from '@/components/Modal'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import { useMemo, useState } from 'react'; +import { useProjectFlockClosingStore } from '@/stores/production/project-flock-closing/project-flock-closing.store'; +import { useMemo } from 'react'; import toast from 'react-hot-toast'; import { useRouter } from 'next/navigation'; import { ApprovalApi } from '@/services/api/approval'; @@ -53,9 +52,8 @@ const ProjectFlockClosingForm = ({ projectFlockKandang: ProjectFlockKandang; }) => { const router = useRouter(); - const closeModal = useModal(); - const [isClosingLoading, setIsClosingLoading] = useState(false); + const { openClosingModal } = useProjectFlockClosingStore(); const { data: closingData, isLoading } = useSWR( `${ProjectFlockKandangApi.basePath}/${projectFlockKandang.id}/closing`, @@ -80,29 +78,34 @@ const ProjectFlockClosingForm = ({ : true; }, [projectFlockKandangApprovals]); - const confirmationModalCloseClickHandler = async () => { - setIsClosingLoading(true); - const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( - projectFlockKandang?.id as number, - { - closed_date: !isKandangClosed - ? formatDate(new Date(), 'YYYY-MM-DD') - : '', - action: !isKandangClosed ? 'close' : 'unclose', - } - ); - - if (isResponseSuccess(deleteProjectFlockRes)) { - toast.success(deleteProjectFlockRes?.message as string); - router.push( - `/production/project-flock/detail?projectFlockId=${projectFlock.id}` + const handleCloseClick = () => { + const closingCallback = async (action: 'close' | 'unclose') => { + const deleteProjectFlockRes = await ProjectFlockKandangApi.closing( + projectFlockKandang?.id as number, + { + closed_date: + action === 'close' ? formatDate(new Date(), 'YYYY-MM-DD') : '', + action, + } ); - } - if (isResponseError(deleteProjectFlockRes)) { - toast.error(deleteProjectFlockRes?.message as string); - } - setIsClosingLoading(false); - closeModal.closeModal(); + + if (isResponseSuccess(deleteProjectFlockRes)) { + toast.success(deleteProjectFlockRes?.message as string); + router.push( + `/production/project-flock/detail?projectFlockId=${projectFlock.id}` + ); + } + if (isResponseError(deleteProjectFlockRes)) { + toast.error(deleteProjectFlockRes?.message as string); + } + }; + + openClosingModal( + projectFlockKandang, + projectFlock.id, + isKandangClosed, + closingCallback + ); }; // const errorStock = useMemo(() => { @@ -334,7 +337,7 @@ const ProjectFlockClosingForm = ({ color='error' isLoading={isLoading} disabled={!isCanCloseValid} - onClick={() => closeModal.openModal()} + onClick={handleCloseClick} >
- -
); diff --git a/src/stores/production/project-flock-closing/project-flock-closing.store.ts b/src/stores/production/project-flock-closing/project-flock-closing.store.ts new file mode 100644 index 00000000..b6543b97 --- /dev/null +++ b/src/stores/production/project-flock-closing/project-flock-closing.store.ts @@ -0,0 +1,19 @@ +'use client'; + +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { createProjectFlockClosingSlice } from '@/stores/production/project-flock-closing/slices/project-flock-closing.slice'; +import { ProjectFlockClosingSlice } from '@/stores/production/project-flock-closing/slices/project-flock-closing.slice'; + +export type ProjectFlockClosingStore = ProjectFlockClosingSlice; + +export const useProjectFlockClosingStore = create()( + devtools( + (...args) => ({ + ...createProjectFlockClosingSlice(...args), + }), + { + name: 'ProjectFlockClosingStore', + } + ) +); diff --git a/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts b/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts new file mode 100644 index 00000000..faff8816 --- /dev/null +++ b/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts @@ -0,0 +1,70 @@ +import { StateCreator } from 'zustand'; +import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; + +export type ProjectFlockClosingSlice = { + // State + isClosingModalOpen: boolean; + selectedProjectFlockKandang: ProjectFlockKandang | null; + projectFlockId: number | null; + isKandangClosed: boolean; + isClosingLoading: boolean; + closingCallback: ((action: 'close' | 'unclose') => Promise) | null; + + // Actions + openClosingModal: ( + data: ProjectFlockKandang, + projectFlockId: number, + isClosed: boolean, + callback: (action: 'close' | 'unclose') => Promise + ) => void; + closeClosingModal: () => void; + setClosingLoading: (loading: boolean) => void; + resetClosing: () => void; +}; + +export const createProjectFlockClosingSlice: StateCreator< + ProjectFlockClosingSlice, + [], + [], + ProjectFlockClosingSlice +> = (set) => ({ + // Initial state + isClosingModalOpen: false, + selectedProjectFlockKandang: null, + projectFlockId: null, + isKandangClosed: false, + isClosingLoading: false, + closingCallback: null, + + // Actions + openClosingModal: (data, projectFlockId, isClosed, callback) => + set({ + isClosingModalOpen: true, + selectedProjectFlockKandang: data, + projectFlockId, + isKandangClosed: isClosed, + closingCallback: callback, + }), + + closeClosingModal: () => + set({ + isClosingModalOpen: false, + selectedProjectFlockKandang: null, + projectFlockId: null, + isKandangClosed: false, + closingCallback: null, + }), + + setClosingLoading: (loading) => + set({ isClosingLoading: loading }), + + resetClosing: () => + set({ + isClosingModalOpen: false, + selectedProjectFlockKandang: null, + projectFlockId: null, + isKandangClosed: false, + isClosingLoading: false, + closingCallback: null, + }), +}); From a0e79168b28df0e7558b61f5ca8e507682038c60 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 20 Feb 2026 10:25:30 +0700 Subject: [PATCH 146/149] refactor(FE): Refactor setClosingLoading to use single-line syntax --- .../slices/project-flock-closing.slice.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts b/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts index faff8816..ccffd387 100644 --- a/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts +++ b/src/stores/production/project-flock-closing/slices/project-flock-closing.slice.ts @@ -55,8 +55,7 @@ export const createProjectFlockClosingSlice: StateCreator< closingCallback: null, }), - setClosingLoading: (loading) => - set({ isClosingLoading: loading }), + setClosingLoading: (loading) => set({ isClosingLoading: loading }), resetClosing: () => set({ From 1f2f3acebb4e464812da5434988574678f9ca77c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 20 Feb 2026 14:17:26 +0700 Subject: [PATCH 147/149] refactor(FE): Remove unused imports and redundant code --- src/app/closing/detail/page.tsx | 1 - src/app/finance/detail/edit/page.tsx | 1 - src/app/finance/detail/page.tsx | 2 +- src/app/page.tsx | 3 +- src/app/production/project-flock/add/page.tsx | 4 +- .../project-flock/detail/edit/page.tsx | 9 ++- .../production/project-flock/detail/page.tsx | 10 ++-- src/components/MainDrawer.tsx | 2 - src/components/Tabs.tsx | 2 +- .../helper/skeleton/DataStateSkeleton.tsx | 1 - src/components/input/PatternInput.tsx | 1 - .../modal/ConfirmationModalWithNotes.tsx | 2 +- .../closing/table/OverheadClosingTable.tsx | 4 +- .../pages/dashboard/export/DashboardPDF.ts | 2 +- src/components/pages/finance/FinanceTable.tsx | 1 - .../FormFinanceAddInitialBalance.tsx | 8 +-- .../add/injection/FormFinanceInjection.tsx | 5 +- .../adjustment/InventoryAdjustmentTable.tsx | 1 - .../form/InventoryAdjustmentForm.tsx | 1 - .../inventory/movement/form/MovementForm.tsx | 1 - .../product/InventoryProductTable.tsx | 7 +-- .../detail/StockProductWarehouseTable.tsx | 5 +- .../marketing/DeliveryOrderFormModal.tsx | 58 +++++-------------- .../pages/marketing/MarketingTable.tsx | 10 +--- .../pages/marketing/SalesOrderFormModal.tsx | 4 +- .../delivery-order/DeliverOrderProduct.tsx | 13 ++--- .../sales-order/SalesOrderProductForm.tsx | 12 ++-- .../marketing/pdf/DeliveryOrderExport.tsx | 2 +- .../pages/marketing/pdf/SalesOrderExport.tsx | 6 +- .../customer/form/CustomerForm.tsx | 3 +- .../master-data/kandang/form/KandangForm.tsx | 3 +- .../location/form/LocationForm.tsx | 3 +- .../nonstock/form/NonstockForm.tsx | 3 +- .../ProductionStandardTable.tsx | 14 ++--- .../warehouse/form/WarehouseForm.tsx | 3 +- .../production/chickin/form/ChickinForm.tsx | 5 -- .../chickin/form/tabs/ChickinFormView.tsx | 2 - .../ProjectFlockConfirmationModal.tsx | 8 +-- .../project-flock/ProjectFlockTable.tsx | 38 +----------- .../closing/ProjectFlockClosingForm.tsx | 16 ----- .../detail/ProjectFlockDetail.tsx | 14 ----- .../project-flock/form/ProjectFlockForm.tsx | 16 +++-- .../form/ProjectFlockKandangTable.tsx | 9 +-- .../TransferToLayingConfirmationModal.tsx | 4 +- .../TransferToLayingDetailModal.tsx | 5 +- .../TransferToLayingFormModal.tsx | 25 +++----- .../form/TransferToLayingForm.schema.ts | 3 - .../order/PurchaseOrderAcceptApprovalForm.tsx | 15 ----- .../form/request/PurchaseRequestForm.tsx | 8 +-- .../purchase/order/PurchaseOrderDetail.tsx | 9 ++- .../export/ReportExpenseExportXLSX.tsx | 2 +- .../export/CustomerPaymentExportPDF.tsx | 2 +- .../finance/export/DebtSupllierExportPDF.tsx | 2 +- .../finance/export/DebtSupplierExportXLSX.tsx | 2 +- .../finance/skeleton/DebtSupplierSkeleton.tsx | 1 - .../report/finance/tab/DebtSupplierTab.tsx | 6 +- .../export/PurchasesPerSupplierExportPDF.tsx | 2 +- .../tab/PurchasesPerSupplierTab.tsx | 5 -- .../export/DailyMarketingExportPDF.tsx | 2 +- .../export/HppPerkandangExportPDF.tsx | 2 +- .../report/marketing/tab/HppPerKandangTab.tsx | 5 -- ...oductionResultProjectFlockKandangTable.tsx | 1 - .../components/base/date-picker.tsx | 18 +++--- .../components/base/date-range-picker.tsx | 5 -- .../daily-checklist/DailyChecklistContent.tsx | 23 ++++---- .../components/pages/dashboard/Dashboard.tsx | 24 ++++---- .../ListDailyChecklistContent.tsx | 16 ++--- .../detail/DetailDailyChecklistContent.tsx | 31 +++++----- .../activity/MasterAktivitasContent.tsx | 14 +---- .../MasterConfigurationContent.tsx | 6 +- .../employee/MasterEmployeeContent.tsx | 23 ++++---- .../reports/DailyChecklistReportsContent.tsx | 58 +++++++++++-------- .../api/daily-checklist/daily-checklist.ts | 3 +- src/services/api/expense.ts | 1 - src/services/api/marketing/marketing.ts | 8 +-- .../api/production/transfer-to-laying.ts | 6 +- src/services/api/report.ts | 3 +- src/services/api/report/marketing-report.ts | 4 +- src/types/api/closing.d.ts | 3 - src/types/api/daily-checklist/employee.d.ts | 2 - src/types/api/dashboard/dashboard.d.ts | 2 - src/types/api/expense.d.ts | 8 +-- src/types/api/inventory/product.d.ts | 1 - src/types/api/marketing/marketing.d.ts | 1 - src/types/api/master-data/nonstock.d.ts | 2 +- src/types/api/master-data/product.d.ts | 2 +- .../api/production/project-flock-kandang.d.ts | 1 - .../api/production/transfer-to-laying.d.ts | 9 +-- src/types/api/report/marketing.d.ts | 5 +- src/types/api/report/report-expense.d.ts | 1 - 90 files changed, 222 insertions(+), 474 deletions(-) diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 96487258..d83b7608 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -7,7 +7,6 @@ import ClosingDetail from '@/components/pages/closing/ClosingDetailTabs'; import { ClosingApi } from '@/services/api/closing'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { FlockApi } from '@/services/api/master-data'; import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { ProjectFlockKandangApi } from '@/services/api/production'; diff --git a/src/app/finance/detail/edit/page.tsx b/src/app/finance/detail/edit/page.tsx index 93a0daea..331f4101 100644 --- a/src/app/finance/detail/edit/page.tsx +++ b/src/app/finance/detail/edit/page.tsx @@ -5,7 +5,6 @@ import useSWR from 'swr'; import { FinanceApi } from '@/services/api/finance'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd'; -import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance'; const EditFinanceTransactionPage = () => { const router = useRouter(); diff --git a/src/app/finance/detail/page.tsx b/src/app/finance/detail/page.tsx index b80e8acb..f23d7770 100644 --- a/src/app/finance/detail/page.tsx +++ b/src/app/finance/detail/page.tsx @@ -4,7 +4,7 @@ import FinanceDetail from '@/components/pages/finance/FinanceDetail'; import useSWR from 'swr'; import { useRouter, useSearchParams } from 'next/navigation'; import { FinanceApi } from '@/services/api/finance'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseSuccess } from '@/lib/api-helper'; const FinanceDetailPage = () => { const router = useRouter(); diff --git a/src/app/page.tsx b/src/app/page.tsx index 33d01de7..8c10b702 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,10 +3,9 @@ import { useEffect } from 'react'; import { usePathname, useRouter } from 'next/navigation'; import { useAuth } from '@/services/hooks/useAuth'; -import { redirectToSSO } from '@/lib/auth-helper'; export default function Home() { - const { user, isLoadingUser } = useAuth(); + const { isLoadingUser } = useAuth(); const router = useRouter(); const pathname = usePathname(); diff --git a/src/app/production/project-flock/add/page.tsx b/src/app/production/project-flock/add/page.tsx index 2eb2c090..eb2b6dd1 100644 --- a/src/app/production/project-flock/add/page.tsx +++ b/src/app/production/project-flock/add/page.tsx @@ -1,8 +1,8 @@ 'use client'; import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm'; -import React, { useImperativeHandle } from 'react'; -import toast from 'react-hot-toast'; +import React from 'react'; +// import React, { useImperativeHandle } from 'react'; const AddProjectFlock = () => { // useImperativeHandle(ref, () => ({ diff --git a/src/app/production/project-flock/detail/edit/page.tsx b/src/app/production/project-flock/detail/edit/page.tsx index e5f88f19..4551dd85 100644 --- a/src/app/production/project-flock/detail/edit/page.tsx +++ b/src/app/production/project-flock/detail/edit/page.tsx @@ -12,11 +12,10 @@ const ProjectFlockEdit = () => { const projectFlockId = searchParams.get('projectFlockId'); - const { - data: projectFlock, - isLoading: isLoadingProjectFlock, - mutate: refreshProjectFlocks, - } = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id)); + const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR( + projectFlockId, + (id: number) => ProjectFlockApi.getSingle(id) + ); if (!projectFlockId) { router.back(); diff --git a/src/app/production/project-flock/detail/page.tsx b/src/app/production/project-flock/detail/page.tsx index 6187898e..f4d58f9a 100644 --- a/src/app/production/project-flock/detail/page.tsx +++ b/src/app/production/project-flock/detail/page.tsx @@ -1,7 +1,6 @@ 'use client'; import ProjectFlockDetail from '@/components/pages/production/project-flock/detail/ProjectFlockDetail'; -import ProjectFlockForm from '@/components/pages/production/project-flock/form/ProjectFlockForm'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { ProjectFlockApi } from '@/services/api/production/project-flock'; import { useRouter, useSearchParams } from 'next/navigation'; @@ -13,11 +12,10 @@ const ProjectFlockDetailPage = () => { const projectFlockId = searchParams.get('projectFlockId'); - const { - data: projectFlock, - isLoading: isLoadingProjectFlock, - mutate: refreshProjectFlock, - } = useSWR(projectFlockId, (id: number) => ProjectFlockApi.getSingle(id)); + const { data: projectFlock, isLoading: isLoadingProjectFlock } = useSWR( + projectFlockId, + (id: number) => ProjectFlockApi.getSingle(id) + ); if (!projectFlockId) { router.back(); diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index 71da0789..724e4b0a 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useCallback } from 'react'; import { usePathname } from 'next/navigation'; import Image from 'next/image'; @@ -13,7 +12,6 @@ import PermissionNotFound from '@/components/helper/PermissionNotFound'; import { useUiStore } from '@/stores/ui/ui.store'; import { MAIN_DRAWER_LINKS } from '@/config/constant'; -import { isPathActive } from '@/lib/helper'; import { ROUTE_PERMISSIONS } from '@/config/route-permission'; import { useAuth } from '@/services/hooks/useAuth'; diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 8a06f9ed..52047d8b 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes, ReactNode, useEffect, useState } from 'react'; +import { HTMLAttributes, ReactNode, useState } from 'react'; import { cn } from '@/lib/helper'; export interface TabItem { diff --git a/src/components/helper/skeleton/DataStateSkeleton.tsx b/src/components/helper/skeleton/DataStateSkeleton.tsx index cd5474e0..f3e9fdef 100644 --- a/src/components/helper/skeleton/DataStateSkeleton.tsx +++ b/src/components/helper/skeleton/DataStateSkeleton.tsx @@ -1,5 +1,4 @@ import IconSkeleton from '@/components/helper/skeleton/IconSkeleton'; -import { Icon } from '@iconify/react'; const DataStateSkeleton = ({ icon, diff --git a/src/components/input/PatternInput.tsx b/src/components/input/PatternInput.tsx index 9af1b68e..290614c7 100644 --- a/src/components/input/PatternInput.tsx +++ b/src/components/input/PatternInput.tsx @@ -4,7 +4,6 @@ import { ChangeEvent } from 'react'; import { PatternFormat, NumberFormatBase, - NumberFormatBaseProps, OnValueChange, } from 'react-number-format'; import TextInput, { TextInputProps } from '@/components/input/TextInput'; diff --git a/src/components/modal/ConfirmationModalWithNotes.tsx b/src/components/modal/ConfirmationModalWithNotes.tsx index e862dffc..20f63019 100644 --- a/src/components/modal/ConfirmationModalWithNotes.tsx +++ b/src/components/modal/ConfirmationModalWithNotes.tsx @@ -56,7 +56,7 @@ const ConfirmationModalWithNotes: React.FC = ({ closeOnBackdrop={closeOnBackdrop} primaryButton={{ ...primaryButton, - onClick: (e) => { + onClick: () => { if (primaryButton && primaryButton?.onClick) { primaryButton?.onClick?.(notes); } else { diff --git a/src/components/pages/closing/table/OverheadClosingTable.tsx b/src/components/pages/closing/table/OverheadClosingTable.tsx index a6c31e6c..421817f9 100644 --- a/src/components/pages/closing/table/OverheadClosingTable.tsx +++ b/src/components/pages/closing/table/OverheadClosingTable.tsx @@ -1,5 +1,5 @@ import Card from '@/components/Card'; -import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; +import Table from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { ClosingApi } from '@/services/api/closing'; @@ -38,7 +38,7 @@ const OverheadClosingTable = ({ } ); - const { data: overheadKandang, isLoading: isLoadingOverheadKandang } = useSWR( + const { data: overheadKandang } = useSWR( kandangId ? `${ClosingApi.basePath}/${projectFlockId}/${kandangId}/overhead` : undefined, diff --git a/src/components/pages/dashboard/export/DashboardPDF.ts b/src/components/pages/dashboard/export/DashboardPDF.ts index 8b4c7e6a..340abcbc 100644 --- a/src/components/pages/dashboard/export/DashboardPDF.ts +++ b/src/components/pages/dashboard/export/DashboardPDF.ts @@ -256,7 +256,7 @@ export const generateDashboardPDF = async ({ pdf.save(fileName); toast.success('PDF exported successfully!', { id: 'export-pdf' }); - } catch (error) { + } catch { toast.error('Failed to export PDF. Please try again.', { id: 'export-pdf', }); diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 6f422753..f83fa469 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -1,6 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { CellContext } from '@tanstack/react-table'; -import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { useFormik } from 'formik'; diff --git a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx index a63caa94..621557b2 100644 --- a/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx +++ b/src/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.tsx @@ -48,9 +48,9 @@ const FormFinanceAddInitialBalance = ({ // ===== Formik ===== const formikInitialValues = useMemo((): InitialBalanceFormValues => { // Type assertion to handle potential initial_balance_type field - const extendedInitialValues = initialValues as Finance & { - initial_balance_type?: string; - }; + // const extendedInitialValues = initialValues as Finance & { + // initial_balance_type?: string; + // }; return { party_type_option: @@ -122,8 +122,6 @@ const FormFinanceAddInitialBalance = ({ options: bankOptions, rawData: bankRawData, isLoadingOptions: isLoadingBankOptions, - setInputValue: setBankInputValue, - loadMore: loadMoreBankOptions, } = useSelect(BankApi.basePath, 'id', 'name'); // ===== Helper Functions ===== diff --git a/src/components/pages/finance/add/injection/FormFinanceInjection.tsx b/src/components/pages/finance/add/injection/FormFinanceInjection.tsx index a4b77baf..b729ce11 100644 --- a/src/components/pages/finance/add/injection/FormFinanceInjection.tsx +++ b/src/components/pages/finance/add/injection/FormFinanceInjection.tsx @@ -28,10 +28,7 @@ import { useCallback, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import Alert from '@/components/Alert'; import { Icon } from '@iconify/react'; -import { - FINANCE_INJECTION_STATUS, - FINANCE_INJECTION_TYPE_OPTIONS, -} from '@/config/constant'; +import { FINANCE_INJECTION_TYPE_OPTIONS } from '@/config/constant'; interface FormFinanceInjectionProps { type?: 'add' | 'edit'; diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index 8da2abf8..1bd47caf 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -15,7 +15,6 @@ import { Icon } from '@iconify/react'; import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table'; import { useCallback, useEffect, useState } from 'react'; import useSWR from 'swr'; -import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; const InventoryAdjustmentTable = () => { const { diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index 3bae393d..612fbb20 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -14,7 +14,6 @@ import { InventoryAdjustmentFormSchema, InventoryAdjustmentFormValues, } from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema'; -import useSWR from 'swr'; import { ProductApi, ProductCategoryApi, diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index dbb30314..f723e763 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -33,7 +33,6 @@ import { toast } from 'react-hot-toast'; import { MovementApi } from '@/services/api/inventory'; import FileInput from '@/components/input/FileInput'; import CheckboxInput from '@/components/input/CheckboxInput'; -import Badge from '@/components/Badge'; import Card from '@/components/Card'; import { S3_PUBLIC_BASE_URL } from '@/config/constant'; import { getUniqueFormikErrors } from '@/lib/formik-helper'; diff --git a/src/components/pages/inventory/product/InventoryProductTable.tsx b/src/components/pages/inventory/product/InventoryProductTable.tsx index fce21b2c..316bd103 100644 --- a/src/components/pages/inventory/product/InventoryProductTable.tsx +++ b/src/components/pages/inventory/product/InventoryProductTable.tsx @@ -15,12 +15,7 @@ import { InventoryProductApi } from '@/services/api/inventory'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { InventoryProduct } from '@/types/api/inventory/product'; import { Icon } from '@iconify/react'; -import { - CellContext, - ColumnDef, - Row, - SortingState, -} from '@tanstack/react-table'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; diff --git a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx index 6f48f7cd..aa375bdc 100644 --- a/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx +++ b/src/components/pages/inventory/product/detail/StockProductWarehouseTable.tsx @@ -1,10 +1,7 @@ import Card from '@/components/Card'; import Table from '@/components/Table'; import { formatNumber } from '@/lib/helper'; -import { - InventoryProduct, - ProductWarehouseStock, -} from '@/types/api/inventory/product'; +import { ProductWarehouseStock } from '@/types/api/inventory/product'; const StockProductWarehouseTable = ({ productWarehouseStock, diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index f1d5e3cc..ae559328 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -48,11 +48,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable); const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm); -const DeliveryOrderFormModal = ({ - initialValues, -}: { - initialValues?: Marketing; -}) => { +const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { const router = useRouter(); const searchParams = useSearchParams(); @@ -76,19 +72,14 @@ const DeliveryOrderFormModal = ({ ); }; - const { data: marketing, isLoading: isLoadingMarketing } = useSWR( + const { data: marketing } = useSWR( isModalActionForForm && marketingId ? `detail-marketing-${marketingId}` : undefined, () => MarketingApi.getSingle(Number(marketingId)) ); - const { - approvals, - rawDataApprovals, - isLoading: isLoadingApproval, - refresh: refreshApproval, - } = useApprovalSteps({ + const { rawDataApprovals, refresh: refreshApproval } = useApprovalSteps({ latestApproval: isResponseSuccess(marketing) ? marketing?.data.latest_approval : undefined, @@ -284,29 +275,10 @@ const DeliveryOrderFormModal = ({ setIsLoading(false); }; - const memoSalesOrder = formik.values.sales_order; - // ================== HANDLER ================== - const nextButtonHandler = () => { - setStep(step + 1); - }; const prevButtonHandler = () => { setStep(step - 1); }; - const handleChangeCustomer = useCallback( - (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('customer_id', (val as OptionType)?.value); - formik.setFieldValue('customer', val as OptionType); - }, - [] - ); - const handleChangeSalesPerson = useCallback( - (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('sales_person_id', (val as OptionType)?.value); - formik.setFieldValue('sales_person', val as OptionType); - }, - [] - ); const rejectMarketingHandler = async (notes: string) => { if (!marketingId) { toast.error(`Tidak ada data yang valid untuk di reject.`); @@ -507,18 +479,18 @@ const DeliveryOrderFormModal = ({ }, []); // ================== MEMOIZED ================== - const isNextButtonDisabled = useMemo(() => { - if (step === 1) { - return Boolean( - !formik.values.customer_id || - !formik.values.sales_person_id || - !formik.values.so_date || - !formik.values.notes - ); - } + // const isNextButtonDisabled = useMemo(() => { + // if (step === 1) { + // return Boolean( + // !formik.values.customer_id || + // !formik.values.sales_person_id || + // !formik.values.so_date || + // !formik.values.notes + // ); + // } - return true; - }, [step, formik.values]); + // return true; + // }, [step, formik.values]); const deliveryRejected = useMemo(() => { return ( isResponseSuccess(marketing) && @@ -877,7 +849,7 @@ const DeliveryOrderFormModal = ({ text: 'Oke', color: 'primary', className: 'rounded-lg', - onClick: (e) => { + onClick: () => { closeModalHandler(); }, }} diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 6368df11..994e8b4c 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -25,7 +25,6 @@ import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import RequirePermission from '@/components/helper/RequirePermission'; -import { useAuth } from '@/services/hooks/useAuth'; import ButtonFilter from '@/components/helper/ButtonFilter'; import Dropdown from '@/components/Dropdown'; import PopoverButton from '@/components/popover/PopoverButton'; @@ -136,7 +135,7 @@ const RowsOptionsMenu = ({ onClick={deleteClickHandler} variant='ghost' color='none' - className='relative p-3 overflow-hidden justify-start text-sm font-semibold w-full text-error before:content-[""] before:absolute before:h-0.25 before:p-3 before:top-0 before:left-0 before:right-0 before:border-t before:border-base-content/5' + className='relative p-3 overflow-hidden justify-start text-sm font-semibold w-full text-error before:content-[""] before:absolute before:h-px before:p-3 before:top-0 before:left-0 before:right-0 before:border-t before:border-base-content/5' > Delete Item @@ -149,14 +148,11 @@ const RowsOptionsMenu = ({ }; const MarketingTable = () => { - const [search, setSearch] = useState(''); - const [approveAction, setApproveAction] = useState<'APPROVED' | 'REJECTED'>( 'APPROVED' ); const [selectedItem, setSelectedItem] = useState(null); const [rowSelection, setRowSelection] = useState>({}); - const { permissionCheck } = useAuth(); const router = useRouter(); const deleteModal = useModal(); @@ -530,7 +526,7 @@ const MarketingTable = () => { {idsToProcess.length > 0 && ( <> -
+
{type !== 'detail' && ( - - diff --git a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx index 33ad2608..429538a6 100644 --- a/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx +++ b/src/figma-make/components/pages/master-data/configuration/MasterConfigurationContent.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Plus, MoreVertical, Pencil, Trash2 } from 'lucide-react'; import { Card, CardContent } from '@/figma-make/components/base/card'; import { Button } from '@/figma-make/components/base/button'; @@ -295,10 +295,6 @@ export function MasterConfigurationContent() { } }; - const handleExport = (format: string) => { - toast.success(`Data berhasil diekspor ke ${format}`); - }; - if (isLoadingDailyChecklistConfigurations && !dailyChecklistConfigurations) { return (
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 f8b67e7a..099aa32a 100644 --- a/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx +++ b/src/figma-make/components/pages/master-data/employee/MasterEmployeeContent.tsx @@ -1,15 +1,7 @@ 'use client'; import { useState } from 'react'; -import { - Plus, - Download, - ChevronDown, - MoreVertical, - Pencil, - Trash2, - Search, -} from 'lucide-react'; +import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react'; import { Card, CardContent } from '@/figma-make/components/base/card'; import { Button } from '@/figma-make/components/base/button'; import { Label } from '@/figma-make/components/base/label'; @@ -93,11 +85,16 @@ export function MasterEmployeeContent() { keepPreviousData: true, } ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); const [showModal, setShowModal] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -373,7 +370,7 @@ export function MasterEmployeeContent() { updateFilter('status', value === 'all' ? '' : value); }} > - + diff --git a/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx b/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx index f1c32198..9c040e33 100644 --- a/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx +++ b/src/figma-make/components/pages/reports/DailyChecklistReportsContent.tsx @@ -2,7 +2,6 @@ import { useMemo } from 'react'; import { Card, CardContent } from '@/figma-make/components/base/card'; -import { Badge } from '@/figma-make/components/base/badge'; import { Label } from '@/figma-make/components/base/label'; import { Select, @@ -11,8 +10,6 @@ import { SelectTrigger, SelectValue, } from '@/figma-make/components/base/select'; -import { toast } from 'sonner'; -import { useRouter } from 'next/navigation'; import { useSelect } from '@/components/input/SelectInput'; import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data'; import useSWR from 'swr'; @@ -26,7 +23,6 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { cn } from '@/lib/helper'; import { ColumnDef } from '@tanstack/react-table'; -import { report } from 'process'; import { PhaseApi } from '@/services/api/daily-checklist/phase'; import { EmployeeApi } from '@/services/api/daily-checklist/employee'; import { Button } from '@/figma-make/components/base/button'; @@ -66,8 +62,6 @@ const YEAR_OPTIONS = [ // }; export function DailyChecklistReportsContent() { - const router = useRouter(); - const currentMonth = useMemo(() => new Date().getMonth() + 1, []); const currentYear = useMemo(() => new Date().getFullYear(), []); @@ -100,11 +94,7 @@ export function DailyChecklistReportsContent() { }, }); - const { - data: reportResponse, - isLoading: isLoadingReport, - mutate: refreshReport, - } = useSWR< + const { data: reportResponse, isLoading: isLoadingReport } = useSWR< BaseApiResponse, AxiosError, SWRHttpKey @@ -116,7 +106,7 @@ export function DailyChecklistReportsContent() { } ); - const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + const { options: areaOptions } = useSelect( AreaApi.basePath, 'id', 'name', @@ -127,33 +117,53 @@ export function DailyChecklistReportsContent() { } ); - const { options: locationOptions, isLoadingOptions: isLoadingLocations } = - useSelect(LocationApi.basePath, 'id', 'name', 'search', { + const { options: locationOptions } = useSelect( + LocationApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', area_id: tableFilterState.area_id, - }); + } + ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', area_id: tableFilterState.area_id, location_id: tableFilterState.location_id, - }); + } + ); - const { options: phaseOptions, isLoadingOptions: isLoadingPhases } = - useSelect(PhaseApi.basePath, 'id', 'name', 'search', { + const { options: phaseOptions } = useSelect( + PhaseApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); - const { options: employeeOptions, isLoadingOptions: isLoadingEmployees } = - useSelect(EmployeeApi.basePath, 'id', 'name', 'search', { + const { options: employeeOptions } = useSelect( + EmployeeApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '500', kandang_id: tableFilterState.kandang_id, - }); + } + ); const currentMonthMaxDay = new Date( Number(tableFilterState.tahun), diff --git a/src/services/api/daily-checklist/daily-checklist.ts b/src/services/api/daily-checklist/daily-checklist.ts index b8f72201..2ea3991c 100644 --- a/src/services/api/daily-checklist/daily-checklist.ts +++ b/src/services/api/daily-checklist/daily-checklist.ts @@ -12,7 +12,6 @@ import { } from '@/types/api/daily-checklist/daily-checklist'; import { isResponseError } from '@/lib/api-helper'; import { toast } from 'sonner'; -import { formatDate } from '@/lib/helper'; export class DailyChecklistApiService extends BaseApiService< DailyChecklist, @@ -316,7 +315,7 @@ export class DailyChecklistApiService extends BaseApiService< wb, `laporan-daily-checklist-${params.get('tahun')}-${params.get('bulan')}.xlsx` ); - } catch (error) { + } catch { toast.error('Gagal melakukan export daily checklist! Coba lagi.'); } } diff --git a/src/services/api/expense.ts b/src/services/api/expense.ts index 2a2fb1a7..b9256506 100644 --- a/src/services/api/expense.ts +++ b/src/services/api/expense.ts @@ -1,5 +1,4 @@ import axios from 'axios'; -import { sleep } from '@/lib/helper'; import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse, GroupedApprovals } from '@/types/api/api-general'; import { diff --git a/src/services/api/marketing/marketing.ts b/src/services/api/marketing/marketing.ts index 76f9b2ea..0a34009b 100644 --- a/src/services/api/marketing/marketing.ts +++ b/src/services/api/marketing/marketing.ts @@ -17,12 +17,6 @@ import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; * 💡 Helper untuk membuat respons dummy * @param data Data yang akan dimasukkan ke dalam body respons */ -const createDummyResponse = (data: T): BaseApiResponse => ({ - code: 200, - status: 'success', - message: 'Data retrieved successfully (MOCK)', - data: data, -}); export class SalesOrderService extends BaseApiService< Marketing, @@ -168,7 +162,7 @@ class MarketingExportService extends BaseApiService< // triggers download in browser XLSX.writeFile(wb, 'marketing.xlsx'); - } catch (error) { + } catch { toast.error('Gagal melakukan export marketing! Coba lagi.'); } } diff --git a/src/services/api/production/transfer-to-laying.ts b/src/services/api/production/transfer-to-laying.ts index 27c88536..6b4beed7 100644 --- a/src/services/api/production/transfer-to-laying.ts +++ b/src/services/api/production/transfer-to-laying.ts @@ -178,7 +178,7 @@ export class TransferToLayingService extends BaseApiService< }); return mappedFlockKandangsAvailableQty; - } catch (error) { + } catch { return undefined; } } @@ -219,7 +219,7 @@ export class TransferToLayingService extends BaseApiService< }); return mappedFlockKandangsMaxTargetQty; - } catch (error) { + } catch { return undefined; } } @@ -273,7 +273,7 @@ export class TransferToLayingService extends BaseApiService< // triggers download in browser XLSX.writeFile(wb, 'transfer-ke-laying.xlsx'); - } catch (error) { + } catch { toast.error('Gagal melakukan export transfer to laying! Coba lagi.'); } } diff --git a/src/services/api/report.ts b/src/services/api/report.ts index d5061d33..5a5e06c8 100644 --- a/src/services/api/report.ts +++ b/src/services/api/report.ts @@ -1,8 +1,7 @@ import { BaseApiService } from '@/services/api/base'; -import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { httpClientFetcher } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { ReportExpense } from '@/types/api/report/report-expense'; -import axios from 'axios'; export class ReportExpenseApiService extends BaseApiService< ReportExpense, diff --git a/src/services/api/report/marketing-report.ts b/src/services/api/report/marketing-report.ts index f55336ac..0f7c5cbb 100644 --- a/src/services/api/report/marketing-report.ts +++ b/src/services/api/report/marketing-report.ts @@ -8,7 +8,7 @@ import { DailyMarketingReport, DailyMarketingReportResponse, } from '@/types/api/report/marketing'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { formatDate } from '@/lib/helper'; export class MarketingReportApiService extends BaseApiService< @@ -68,7 +68,7 @@ export class MarketingReportApiService extends BaseApiService< // triggers download in browser XLSX.writeFile(wb, 'laporan-penjualan-harian.xlsx'); - } catch (error) { + } catch { toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); } } diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts index 6bf8c7c6..eca09586 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -1,6 +1,3 @@ -import { Area } from '@/types/api/master-data/area'; -import { Flock } from '@/types/api/master-data/flock'; -import { Location } from '@/types/api/master-data/location'; import { Kandang } from '@/types/api/master-data/kandang'; import { Product } from '@type/api/master-data/product'; import { Customer } from '@type/api/master-data/customer'; diff --git a/src/types/api/daily-checklist/employee.d.ts b/src/types/api/daily-checklist/employee.d.ts index 6010dfa1..36aa1dc4 100644 --- a/src/types/api/daily-checklist/employee.d.ts +++ b/src/types/api/daily-checklist/employee.d.ts @@ -1,6 +1,4 @@ import { BaseMetadata } from '@/types/api/api-general'; -import { BaseLocation } from '@/types/api/master-data/location'; -import { BaseUser } from '@/types/api/user'; import { BaseKandang } from '@/types/api/master-data/kandang'; export type BaseEmployee = { diff --git a/src/types/api/dashboard/dashboard.d.ts b/src/types/api/dashboard/dashboard.d.ts index 749b469a..7622155c 100644 --- a/src/types/api/dashboard/dashboard.d.ts +++ b/src/types/api/dashboard/dashboard.d.ts @@ -1,5 +1,3 @@ -import { SuccessApiResponse } from '@/types/api/api-general'; - export interface Dashboard { statistics_data: DashboardStatisticsData[]; charts: DashboardComparisonCharts | DashboardOverviewCharts; diff --git a/src/types/api/expense.d.ts b/src/types/api/expense.d.ts index 3ca57dd0..abeab48a 100644 --- a/src/types/api/expense.d.ts +++ b/src/types/api/expense.d.ts @@ -1,9 +1,7 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; -import { BaseLocation, Location } from '@/types/api/master-data/location'; -import { BaseKandang, Kandang } from '@/types/api/master-data/kandang'; -import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier'; -import { BaseNonstock, Nonstock } from '@/types/api/master-data/nonstock'; -import { BaseUser } from '@/types/api/user'; +import { BaseLocation } from '@/types/api/master-data/location'; +import { BaseSupplier } from '@/types/api/master-data/supplier'; +import { BaseNonstock } from '@/types/api/master-data/nonstock'; export type BaseExpense = { id: number; diff --git a/src/types/api/inventory/product.d.ts b/src/types/api/inventory/product.d.ts index 134b982a..6789dffc 100644 --- a/src/types/api/inventory/product.d.ts +++ b/src/types/api/inventory/product.d.ts @@ -1,5 +1,4 @@ import { BaseMetadata, CreatedUser } from '@/types/api/api-general'; -import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductCategory } from '@/types/api/master-data/product-category'; import { Supplier } from '@/types/api/master-data/supplier'; import { Uom } from '@/types/api/master-data/uom'; diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index 12b0ee2c..a867d983 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -6,7 +6,6 @@ import { } from '@/types/api/api-general'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { Kandang } from '@/types/api/master-data/kandang'; -import { id } from 'react-day-picker/locale'; import { Warehouse } from '@/types/api/master-data/warehouse'; /** diff --git a/src/types/api/master-data/nonstock.d.ts b/src/types/api/master-data/nonstock.d.ts index e4e79d8e..834c57db 100644 --- a/src/types/api/master-data/nonstock.d.ts +++ b/src/types/api/master-data/nonstock.d.ts @@ -1,4 +1,4 @@ -import { BaseApiResponse, BaseMetadata, flags } from '@/types/api/api-general'; +import { BaseMetadata, flags } from '@/types/api/api-general'; import { BaseUom } from '@/types/api/master-data/uom'; import { BaseSupplier } from '@/types/api/master-data/supplier'; diff --git a/src/types/api/master-data/product.d.ts b/src/types/api/master-data/product.d.ts index 7fd2c7c1..c1b9b4b6 100644 --- a/src/types/api/master-data/product.d.ts +++ b/src/types/api/master-data/product.d.ts @@ -1,7 +1,7 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Uom } from '@/types/api/master-data/uom'; import { ProductCategory } from '@/types/api/master-data/product-category'; -import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier'; +import { BaseSupplier } from '@/types/api/master-data/supplier'; export type BaseProduct = { id: number; diff --git a/src/types/api/production/project-flock-kandang.d.ts b/src/types/api/production/project-flock-kandang.d.ts index 111ca98b..67c3cfae 100644 --- a/src/types/api/production/project-flock-kandang.d.ts +++ b/src/types/api/production/project-flock-kandang.d.ts @@ -1,7 +1,6 @@ import { Kandang } from '@/type/master-data/kandang'; import { ProjectFlock } from '@/types/api/production/project-flock'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; -import { Supplier } from '@/types/api/master-data/supplier'; import { BaseApproval } from '@/types/api/api-general'; export type BaseProjectFlockKandang = { diff --git a/src/types/api/production/transfer-to-laying.d.ts b/src/types/api/production/transfer-to-laying.d.ts index c162ed82..8123e9e6 100644 --- a/src/types/api/production/transfer-to-laying.d.ts +++ b/src/types/api/production/transfer-to-laying.d.ts @@ -1,10 +1,5 @@ -import { - BaseApiResponse, - BaseMetadata, - CreatedUser, - flags, -} from '@/types/api/api-general'; -import { BaseKandang, Kandang } from '@/types/api/master-data/kandang'; +import { BaseMetadata, CreatedUser } from '@/types/api/api-general'; +import { BaseKandang } from '@/types/api/master-data/kandang'; import { WarehouseType } from '@/types/api/master-data/warehouse'; export type BaseTransferToLaying = { diff --git a/src/types/api/report/marketing.d.ts b/src/types/api/report/marketing.d.ts index 20c60725..38797668 100644 --- a/src/types/api/report/marketing.d.ts +++ b/src/types/api/report/marketing.d.ts @@ -1,13 +1,10 @@ import { BaseApiResponse, BaseMetadata } from '@/types/api/api-general'; -import { BaseCustomer, Customer } from '@/types/api/master-data/customer'; +import { BaseCustomer } from '@/types/api/master-data/customer'; import { BaseWarehouseArea, BaseWarehouseKandang, BaseWarehouseLocation, - Warehouse, } from '@/types/api/master-data/warehouse'; -import { Location } from '@/types/api/master-data/location'; -import { Area } from '@/types/api/master-data/area'; import { BaseProduct } from '@/types/api/master-data/product'; import { BaseUser } from '@/types/api/user'; diff --git a/src/types/api/report/report-expense.d.ts b/src/types/api/report/report-expense.d.ts index 3918820d..bf9b94eb 100644 --- a/src/types/api/report/report-expense.d.ts +++ b/src/types/api/report/report-expense.d.ts @@ -1,6 +1,5 @@ import { BaseApproval, CreatedUser } from '@/types/api/api-general'; import { Supplier } from '@/types/api/master-data/supplier'; -import { Location } from '@/types/api/master-data/location'; import { Nonstock } from '@/types/api/master-data/nonstock'; import { Kandang } from '@/types/api/master-data/kandang'; From 1cc0e16c01d3b3d892daf67ceba45f2537aeb463 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 20 Feb 2026 14:41:50 +0700 Subject: [PATCH 148/149] refactor(FE): Replace ButtonFilter with custom Filter button --- .../pages/marketing/MarketingTable.tsx | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 994e8b4c..629ddcf7 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -25,7 +25,6 @@ import { useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; import RequirePermission from '@/components/helper/RequirePermission'; -import ButtonFilter from '@/components/helper/ButtonFilter'; import Dropdown from '@/components/Dropdown'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; @@ -215,6 +214,28 @@ const MarketingTable = () => { updateFilter('customer_id', ''); }; + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + // Product filter + if (tableFilterState.product_ids) { + count += 1; + } + + // Status filter + if (tableFilterState.status) { + count += 1; + } + + // Customer filter + if (tableFilterState.customer_id) { + count += 1; + } + + return count; + }, [tableFilterState.product_ids, tableFilterState.status, tableFilterState.customer_id]); + const approveClickHandler = () => { setApproveAction('APPROVED'); confirmationModal.openModal(); @@ -563,16 +584,27 @@ const MarketingTable = () => { )}
- { - const { ...rest } = tableFilterState; - return rest; - })()} + Date: Fri, 20 Feb 2026 14:42:46 +0700 Subject: [PATCH 149/149] refactor(FE): Fix formatting for array dependencies and className conditions --- src/components/pages/marketing/MarketingTable.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 629ddcf7..0bf00833 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -234,7 +234,11 @@ const MarketingTable = () => { } return count; - }, [tableFilterState.product_ids, tableFilterState.status, tableFilterState.customer_id]); + }, [ + tableFilterState.product_ids, + tableFilterState.status, + tableFilterState.customer_id, + ]); const approveClickHandler = () => { setApproveAction('APPROVED'); @@ -593,7 +597,8 @@ const MarketingTable = () => { className={cn( 'px-3 py-2.5 gap-1.5 text-sm text-base-content/50 border border-base-content/10 rounded-xl shadow-button-soft transition-all', { - 'border-primary-gradient text-primary': activeFiltersCount > 0, + 'border-primary-gradient text-primary': + activeFiltersCount > 0, } )} >
+ { @@ -1042,7 +1041,7 @@ const PurchaseOrderDetail = ({ ref={staffApprovalModal.ref} closeOnBackdrop className={{ - modalBox: 'w-full max-w-screen-2xl max-h-[90vh] overflow-y-auto', + modalBox: 'w-full max-w-2xl max-h-[90vh] overflow-y-auto', }} > index + 1, + cell: ({ index }) => index + 1, footer: 'Total', }, { diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index e10839c6..1b5fc933 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -49,7 +49,7 @@ const getTableColumns = (total?: DebtSupplier['total']): PdfColumn[] => { header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, footer: 'Total', }, { diff --git a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx index e5de3ae2..b19ee86e 100644 --- a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx +++ b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx @@ -2,7 +2,7 @@ import ExcelJS from 'exceljs'; import { formatDate } from '@/lib/helper'; -import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier'; +import { DebtSupplier } from '@/types/api/report/debt-supplier'; interface DebtSupplierExportExcelParams { data: DebtSupplier[]; diff --git a/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx b/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx index b9397f8f..385877fc 100644 --- a/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx +++ b/src/components/pages/report/finance/skeleton/DebtSupplierSkeleton.tsx @@ -1,7 +1,6 @@ import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; import Table from '@/components/Table'; import { DebtRow } from '@/types/api/report/debt-supplier'; -import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; const DebtSupplierSkeleton = ({ diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index d0a27b92..635bece8 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -129,7 +129,7 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { filterModal.closeModal(); setIsSubmitted(true); }, - onReset: (values) => { + onReset: () => { setFilterParams({ start_date: undefined, end_date: undefined, @@ -170,10 +170,6 @@ const DebtSupplierTab = ({ tabId }: DebtSupplierTabProps) => { : [], [debtSupplier] ); - const meta = - isResponseSuccess(debtSupplier) && debtSupplier?.meta - ? debtSupplier.meta - : null; // ===== EXPORT DATA FETCHER ===== const debtSupplierExport = useCallback(async (): Promise< diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx index d265d08f..83f34a28 100644 --- a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExportPDF.tsx @@ -61,7 +61,7 @@ const getTableColumns = ( header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, footer: 'Total', }, { diff --git a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx index 36476956..87c5ee8d 100644 --- a/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -336,11 +336,6 @@ const PurchasesPerSupplierTab = ({ tabId }: PurchasesPerSupplierTabProps) => { [purchasePerSupplier] ); - const meta = - isResponseSuccess(purchasePerSupplier) && purchasePerSupplier?.meta - ? purchasePerSupplier.meta - : null; - // ===== EXPORT DATA FETCHER ===== const logisticPurchasePerSupplierExport = useCallback(async (): Promise< LogisticPurchasePerSupplierReport[] | null diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx index 29c1d619..c5e1a3a5 100644 --- a/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx +++ b/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx @@ -52,7 +52,7 @@ const getTableColumns = ( header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, }, { key: 'so_date', diff --git a/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx index f2a3c835..bf68a195 100644 --- a/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx +++ b/src/components/pages/report/marketing/export/HppPerkandangExportPDF.tsx @@ -142,7 +142,7 @@ const getDetailColumns = ( header: 'No', flex: 0.5, align: 'center', - cell: ({ row, index }) => index + 1, + cell: ({ index }) => index + 1, footer: 'TOTAL', }, { diff --git a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx index e106dbf4..991c6546 100644 --- a/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/marketing/tab/HppPerKandangTab.tsx @@ -324,11 +324,6 @@ const HppPerKandangTab = ({ tabId }: HppPerKandangTabProps) => { [hppPerKandang] ); - const period = - isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period - ? hppPerKandang.data.period - : undefined; - // ===== EXPORT DATA FETCHER ===== const hppPerKandangExport = useCallback(async (): Promise => { diff --git a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx index ded97d02..25b1bd28 100644 --- a/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx +++ b/src/components/pages/report/production-result/tab/ProductionResultProjectFlockKandangTable.tsx @@ -28,7 +28,6 @@ const ProductionResultProjectFlockKandangTable = ({ setPage, setPageSize, toQueryString: getTableFilterQueryString, - reset: resetFilter, } = useTableFilter({ initial: { filter_by: '', diff --git a/src/figma-make/components/base/date-picker.tsx b/src/figma-make/components/base/date-picker.tsx index abd3414f..b19fff3d 100644 --- a/src/figma-make/components/base/date-picker.tsx +++ b/src/figma-make/components/base/date-picker.tsx @@ -46,15 +46,15 @@ export function DatePicker({ }); }; - const formatDateInput = (dateStr: string) => { - if (!dateStr) return ''; - const d = new Date(dateStr + 'T00:00:00'); - return d.toLocaleDateString('en-GB', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - }); - }; + // const formatDateInput = (dateStr: string) => { + // if (!dateStr) return ''; + // const d = new Date(dateStr + 'T00:00:00'); + // return d.toLocaleDateString('en-GB', { + // day: '2-digit', + // month: '2-digit', + // year: 'numeric', + // }); + // }; const displayFormatter = formatDisplay || defaultFormatDisplay; diff --git a/src/figma-make/components/base/date-range-picker.tsx b/src/figma-make/components/base/date-range-picker.tsx index d1bca47e..01b197b7 100644 --- a/src/figma-make/components/base/date-range-picker.tsx +++ b/src/figma-make/components/base/date-range-picker.tsx @@ -13,11 +13,6 @@ import { } from '@/figma-make/components/base/popover'; import { Input } from '@/figma-make/components/base/input'; -interface DateRange { - from: string; - to: string; -} - interface DateRangePickerProps { dateFrom: string; dateTo: string; diff --git a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx index 8cef1da7..601025ad 100644 --- a/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx +++ b/src/figma-make/components/pages/daily-checklist/DailyChecklistContent.tsx @@ -86,17 +86,18 @@ export function DailyChecklistContent() { searchParams.get('category') || '' ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); - const { - data: phases, - isLoading: isLoadingPhases, - mutate: refreshPhases, - } = useSWR< + const { data: phases } = useSWR< BaseApiResponse, AxiosError, SWRHttpKey @@ -104,11 +105,7 @@ export function DailyChecklistContent() { keepPreviousData: true, }); - const { - data: employeesRes, - isLoading: isLoadingEmployees, - mutate: refreshEmployees, - } = useSWR( + const { data: employeesRes } = useSWR( `${EmployeeApi.basePath}?page=1&limit=500&kandang_id=${kandangId}&is_active=true`, EmployeeApi.getAllFetcher, { diff --git a/src/figma-make/components/pages/dashboard/Dashboard.tsx b/src/figma-make/components/pages/dashboard/Dashboard.tsx index 8953ccf6..a924c2b3 100644 --- a/src/figma-make/components/pages/dashboard/Dashboard.tsx +++ b/src/figma-make/components/pages/dashboard/Dashboard.tsx @@ -16,12 +16,7 @@ import { SelectValue, } from '@/figma-make/components/base/select'; import { Badge } from '@/figma-make/components/base/badge'; -import { - Calendar as CalendarIcon, - Users, - AlertCircle, - Info, -} from 'lucide-react'; +import { Users, AlertCircle, Info } from 'lucide-react'; import { DateRangePicker } from '@/figma-make/components/base/date-range-picker'; import { BarChart, @@ -71,11 +66,7 @@ export function Dashboard() { const [kandangFilter, setKandangFilter] = useState('ALL'); const [categoryFilter, setCategoryFilter] = useState('ALL'); - const { - data: summaryResponse, - isLoading: isLoadingSummary, - mutate: refreshSummary, - } = useSWR< + const { data: summaryResponse, isLoading: isLoadingSummary } = useSWR< BaseApiResponse, AxiosError, SWRHttpKey @@ -86,11 +77,16 @@ export function Dashboard() { httpClientFetcher ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); const kandangColorMap: { [key: string]: string } = {}; (kandangOptions || []).forEach((k, index) => { 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 634d8716..6509a91d 100644 --- a/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/ListDailyChecklistContent.tsx @@ -38,11 +38,6 @@ import { KandangApi } from '@/services/api/master-data'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import RequirePermission from '@/components/helper/RequirePermission'; -interface Kandang { - id: string; - name: string; -} - const STATUS_OPTIONS = [ { value: 'ALL', label: 'Semua Status' }, { value: 'DRAFT', label: 'Draft' }, @@ -98,11 +93,16 @@ export function ListDailyChecklistContent() { } ); - const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = - useSelect(KandangApi.basePath, 'id', 'name', 'search', { + const { options: kandangOptions } = useSelect( + KandangApi.basePath, + 'id', + 'name', + 'search', + { page: '1', limit: '100', - }); + } + ); const checklistList = isResponseSuccess(checklistListRes) ? checklistListRes.data || [] diff --git a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx index 846a2670..88f04a80 100644 --- a/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx +++ b/src/figma-make/components/pages/list-daily-checklist/detail/DetailDailyChecklistContent.tsx @@ -17,7 +17,7 @@ import { DialogFooter, } from '@/figma-make/components/base/dialog'; import { toast } from 'sonner'; -import { notFound, useRouter, useSearchParams } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { DailyChecklistApi } from '@/services/api/daily-checklist/daily-checklist'; import { isResponseError } from '@/lib/api-helper'; import Link from 'next/link'; @@ -90,16 +90,16 @@ interface ChecklistData { }; } -interface AssignmentQueryResult { - task_id: number; - employee_id: string; - checked: boolean; - note: string | null; - employees: { - id: number; - name: string; - } | null; -} +// interface AssignmentQueryResult { +// task_id: number; +// employee_id: string; +// checked: boolean; +// note: string | null; +// employees: { +// id: number; +// name: string; +// } | null; +// } const CATEGORY_LABELS: { [key: string]: string } = { pullet_open: 'Pullet Open', @@ -124,7 +124,7 @@ export function DetailDailyChecklistContent() { const [loading, setLoading] = useState(true); const [header, setHeader] = useState(null); - const [detailRows, setDetailRows] = useState([]); + const [, setDetailRows] = useState([]); const [phaseGroups, setPhaseGroups] = useState([]); const [employees, setEmployees] = useState<{ id: string; name: string }[]>( [] @@ -381,7 +381,7 @@ export function DetailDailyChecklistContent() { // Convert to array and group by time_type const grouped: PhaseGroup[] = []; - phaseMap.forEach((phaseData, phaseId) => { + phaseMap.forEach((phaseData) => { const timeGroups: { [timeType: string]: { activities: { @@ -570,9 +570,6 @@ export function DetailDailyChecklistContent() { return null; } - const isReadOnly = - header.status === 'APPROVED' || header.status === 'REJECTED'; - return (
@@ -680,7 +677,7 @@ export function DetailDailyChecklistContent() { {header.status === 'REJECTED' && header.reject_reason && (
- +
Nama Aktivitas + Aksi