From 9dc8f0553449ee5b789acebd28308f4bd5553668 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 6 Feb 2026 10:55:38 +0700 Subject: [PATCH 01/59] 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 02/59] 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 03/59] 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 04/59] 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 05/59] 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 06/59] 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 07/59] 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 08/59] 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 09/59] 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 10/59] 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 11/59] 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 12/59] 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 15/59] 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 16/59] 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 17/59] 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 18/59] 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 19/59] 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 20/59] 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 21/59] 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 22/59] 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 23/59] 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 24/59] 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 25/59] 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 26/59] 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 27/59] 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 28/59] 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 29/59] 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 32/59] 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 33/59] 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 34/59] 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 35/59] 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 36/59] 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 37/59] 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 38/59] 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 39/59] 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 40/59] 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 41/59] 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 42/59] 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 43/59] 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 44/59] 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 45/59] 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 46/59] 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 47/59] 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 48/59] 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 49/59] 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 50/59] 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 51/59] 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 52/59] 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 53/59] 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 54/59] 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 55/59] 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 56/59] 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 57/59] 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 58/59] 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 59/59] 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') !==