From 68c1655824be0aa13a1c2b1cf8ae7d3d09d711cb Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Apr 2026 13:57:09 +0700 Subject: [PATCH 01/10] fix: adjust unit_price and price_per_qty value to match the SalesOrderProductForm --- .../marketing/form/MarketingForm.schema.ts | 17 +- .../delivery-order/DeliverOrderProduct.tsx | 158 ++++++++++++++---- 2 files changed, 137 insertions(+), 38 deletions(-) diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index 144ec6ff..3d7eac5c 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -194,6 +194,19 @@ export const mergeSOwithDO = ( const delivery = deliveryOrders.find( (d) => d?.marketing_product_id === so.id ); + const isTelurQty = + so.marketing_type?.value?.toLowerCase() === 'telur' && + so.convertion_unit?.value?.toLowerCase() === 'qty'; + const salesOrderUnitPrice = + isTelurQty && Number(so.total_price || 0) > 0 && Number(so.qty || 0) > 0 + ? Number(so.total_price) / Number(so.qty) + : so.unit_price; + const salesOrderPricePerQty = + isTelurQty && + Number(so.total_price || 0) > 0 && + Number(so.total_weight || 0) > 0 + ? Number(so.total_price) / Number(so.total_weight) + : so.price_per_qty; return { ...so, // nilai dasar dari sales order @@ -201,7 +214,7 @@ export const mergeSOwithDO = ( delivery_date: delivery?.delivery_date || undefined, do_number: delivery?.do_number || undefined, vehicle_number: delivery?.vehicle_number || so.vehicle_number, - unit_price: autofill ? so.unit_price : delivery?.unit_price, + unit_price: autofill ? salesOrderUnitPrice : delivery?.unit_price, total_weight: autofill ? so.total_weight : delivery?.total_weight, qty: autofill ? so.qty : delivery?.qty, avg_weight: autofill ? so.avg_weight : delivery?.avg_weight, @@ -219,7 +232,7 @@ export const mergeSOwithDO = ( : delivery?.convertion_unit, marketing_type: autofill ? so.marketing_type : delivery?.marketing_type, total_peti: autofill ? so.total_peti : delivery?.total_peti, - price_per_qty: autofill ? so.price_per_qty : delivery?.price_per_qty, + price_per_qty: autofill ? salesOrderPricePerQty : delivery?.price_per_qty, sisa_berat: autofill ? so.sisa_berat : delivery?.sisa_berat, price_sisa_berat: autofill ? so.price_sisa_berat 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 66b50600..781ebf26 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -32,6 +32,63 @@ import Dropdown from '@/components/Dropdown'; import { Icon } from '@iconify/react'; import { handleMarketingCalculation } from '@/lib/marketing-calculation'; +type PricingOption = + | string + | { + value: string; + label: string; + } + | null + | undefined; + +type PricingSource = + | { + marketing_type?: PricingOption; + convertion_unit?: PricingOption; + total_price?: string | number | null; + qty?: string | number | null; + total_weight?: string | number | null; + unit_price?: string | number | null; + price_per_qty?: number | null; + } + | null + | undefined; + +const getOptionValue = (value?: PricingOption) => { + if (!value) return undefined; + if (typeof value === 'string') return value.toLowerCase(); + + return value.value?.toLowerCase(); +}; + +const isTelurQtyProduct = (value?: PricingSource) => + getOptionValue(value?.marketing_type) === 'telur' && + getOptionValue(value?.convertion_unit) === 'qty'; + +const getDisplayedUnitPrice = (value?: PricingSource) => { + if ( + isTelurQtyProduct(value) && + Number(value?.total_price || 0) > 0 && + Number(value?.qty || 0) > 0 + ) { + return Number(value?.total_price) / Number(value?.qty); + } + + return value?.unit_price ?? undefined; +}; + +const getDisplayedPricePerQty = (value?: PricingSource) => { + if ( + isTelurQtyProduct(value) && + Number(value?.total_price || 0) > 0 && + Number(value?.total_weight || 0) > 0 + ) { + return Number(value?.total_price) / Number(value?.total_weight); + } + + return value?.price_per_qty ?? null; +}; + const DeliveryOrderProductForm = ({ formState, salesOrders, @@ -69,14 +126,18 @@ const DeliveryOrderProductForm = ({ Number(initialValues.total_peti) : 0; - const initialPricePerConvertion = - initialValues?.total_price && - initialValues?.total_peti && - Number(initialValues.total_peti) !== 0 - ? (Number(initialValues.total_price) - - initialSisaBerat * Number(initialValues.unit_price || 0)) / - Number(initialValues.total_peti) - : 0; + // const initialPricePerConvertion = + // initialValues?.total_price && + // initialValues?.total_peti && + // Number(initialValues.total_peti) !== 0 + // ? (Number(initialValues.total_price) - + // initialSisaBerat * Number(initialValues.unit_price || 0)) / + // Number(initialValues.total_peti) + // : 0; + + const initialPricePerConvertion = initialValues?.unit_price + ? Number(initialValues?.unit_price) + : 0; const initialPriceSisaBerat = initialValues?.total_price && initialValues?.total_peti @@ -154,6 +215,27 @@ const DeliveryOrderProductForm = ({ (item) => item.id === initialValues?.marketing_product_id ); + const defaultPricingSource: PricingSource = { + marketing_type: + initialValues?.marketing_type ?? salesOrder?.marketing_type ?? null, + convertion_unit: + initialValues?.convertion_unit ?? salesOrder?.convertion_unit ?? null, + total_price: + deliveryOrder?.total_price ?? + initialValues?.total_price ?? + salesOrder?.total_price, + qty: deliveryOrder?.qty ?? initialValues?.qty ?? salesOrder?.qty, + total_weight: + deliveryOrder?.total_weight ?? + initialValues?.total_weight ?? + salesOrder?.total_weight, + unit_price: + deliveryOrder?.unit_price ?? + initialValues?.unit_price ?? + salesOrder?.unit_price, + price_per_qty: initialValues?.price_per_qty ?? null, + }; + const formik = useFormik({ enableReinitialize: true, initialValues: { @@ -167,8 +249,7 @@ const DeliveryOrderProductForm = ({ undefined, marketing_product_id: salesOrder?.id || initialValues?.marketing_product_id || undefined, - unit_price: - deliveryOrder?.unit_price ?? initialValues?.unit_price ?? undefined, + unit_price: getDisplayedUnitPrice(defaultPricingSource), total_weight: deliveryOrder?.total_weight ?? initialValues?.total_weight ?? undefined, qty: deliveryOrder?.qty ?? initialValues?.qty ?? undefined, @@ -186,7 +267,7 @@ const DeliveryOrderProductForm = ({ convertion_unit: initialValues?.convertion_unit || null, marketing_type: initialValues?.marketing_type || null, total_peti: initialValues?.total_peti ?? null, - price_per_qty: initialValues?.price_per_qty ?? null, + price_per_qty: getDisplayedPricePerQty(defaultPricingSource), sisa_berat: initialSisaBerat, price_sisa_berat: initialPriceSisaBerat, week: initialValues?.week ?? null, @@ -329,7 +410,11 @@ const DeliveryOrderProductForm = ({ if (!Boolean(initialValues.qty)) { handleResetForm(); } else { - setFormikValues(initialValues); + setFormikValues({ + ...initialValues, + unit_price: getDisplayedUnitPrice(initialValues), + price_per_qty: getDisplayedPricePerQty(initialValues), + }); if (initialValues?.marketing_product_id) { setSelectedProduct({ value: initialValues?.id, @@ -458,10 +543,11 @@ const DeliveryOrderProductForm = ({ marketing_product_id: selected.value as number, marketing_product: soFieldValues, qty: so.qty, - unit_price: so.unit_price, + unit_price: getDisplayedUnitPrice(so), total_price: so.total_price, avg_weight: so.avg_weight, total_weight: so.total_weight, + price_per_qty: getDisplayedPricePerQty(so), vehicle_number: so.vehicle_number, week: soFieldValues.week ?? null, }); @@ -761,12 +847,32 @@ const DeliveryOrderProductForm = ({ /> )} - {/* Harga per butir untuk TELUR + QTY */} + {/* Harga Satuan */} + {formik.values.convertion_unit?.value.toLowerCase() !== 'peti' && + formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && ( + { + const value = Number(e.target.value); + handleFieldChange('unit_price', value, () => + setCurrentInput(e.target.name) + ); + }} + isError={Boolean(formik.errors.unit_price)} + errorMessage={formik.errors.unit_price} + placeholder='Masukan Harga Satuan' + /> + )} + + {/* Harga per kg untuk TELUR + QTY */} {formik.values.marketing_type?.value.toLowerCase() === 'telur' && formik.values.convertion_unit?.value.toLowerCase() === 'qty' && ( { @@ -780,27 +886,7 @@ const DeliveryOrderProductForm = ({ Boolean(formik.errors.price_per_qty) } errorMessage={formik.errors.price_per_qty} - placeholder='Masukan Harga per Butir' - /> - )} - - {/* Harga Satuan */} - {formik.values.convertion_unit?.value.toLowerCase() !== 'peti' && - formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && ( - { - const value = Number(e.target.value); - handleFieldChange('unit_price', value, () => - setCurrentInput(e.target.name) - ); - }} - isError={Boolean(formik.errors.unit_price)} - errorMessage={formik.errors.unit_price} - placeholder='Masukan Harga Satuan' + placeholder='Masukan Harga per Kg' /> )} From 5c39e900f36d16c75cbc40d56d424b7f359770c3 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Apr 2026 15:30:37 +0700 Subject: [PATCH 02/10] chore: remove unnecessary code --- .../form/repeater/sales-order/SalesOrderProductForm.tsx | 5 ----- 1 file changed, 5 deletions(-) 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 a17dbda1..a7f2c73e 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -157,11 +157,6 @@ const SalesOrderProductForm = ({ ); }, [selectedProductWarehouse, formik.values.marketing_type]); - console.log({ - initialValues, - values: formik.values, - }); - // ===== Options ===== const { options: warehouseOptions, From 6c03e420061522fd760128216ec2ad390ea88992 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Apr 2026 15:31:31 +0700 Subject: [PATCH 03/10] fix: remove transport_per_item and vehicle_number value and disable them if expedition vendor is empty --- .../purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 93e15dfb..2eacfbad 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -367,6 +367,9 @@ const PurchaseOrderAcceptApprovalForm = ({ ); } else { formik.setFieldValue(`items.${idx}.expedition_vendor_id`, null); + + formik.setFieldValue(`items.${idx}.transport_per_item`, null); + formik.setFieldValue(`items.${idx}.vehicle_number`, null); } }; @@ -553,6 +556,7 @@ const PurchaseOrderAcceptApprovalForm = ({ ) } onBlur={formik.handleBlur} + disabled={!Boolean(formItem?.expedition_vendor)} isError={ isRepeaterInputError(idx, 'vehicle_number').isError } @@ -657,6 +661,7 @@ const PurchaseOrderAcceptApprovalForm = ({ thousandSeparator=',' decimalSeparator='.' inputPrefix={'Rp'} + disabled={!Boolean(formItem?.expedition_vendor)} isError={ isRepeaterInputError(idx, 'transport_per_item') .isError From 726065da512a890afd52e5545bdcacd5d629de2c Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 8 Apr 2026 15:32:01 +0700 Subject: [PATCH 04/10] fix: make vehicle_number and transport_per_item required if expedition_vendor exist --- .../form/order/PurchaseOrderForm.schema.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index 6a299703..9e6f43b0 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -185,7 +185,12 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema + Boolean(expeditionVendor?.value), + then: (schema) => schema.required('Nomor kendaraan wajib diisi!'), + otherwise: (schema) => schema.optional(), + }) .typeError('Nomor kendaraan harus berupa plat nomor!'), expedition_vendor: Yup.object({ value: Yup.number().min(1).required(), @@ -213,7 +218,13 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema() .nullable() - .optional() + .when('expedition_vendor', { + is: (expeditionVendor?: { value?: number; label?: string } | null) => + Boolean(expeditionVendor?.value), + then: (schema) => + schema.required('Biaya transport per item wajib diisi!'), + otherwise: (schema) => schema.optional(), + }) .test( 'is-valid-transport-per-item', 'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!', From 095b1c5850485b1b9c220799e71741bd6288205d Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Apr 2026 11:04:57 +0700 Subject: [PATCH 05/10] fix: adjust selected delivery product priority order --- src/components/pages/marketing/DeliveryOrderFormModal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 4635c826..eb488285 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -368,7 +368,9 @@ const DeliveryOrderFormModal = ({}: { initialValues?: Marketing }) => { const currentProducts = deliveryOrderValues?.find( (product) => product.id == id ); - setSelectedDeliveryProduct(values ?? currentProducts ?? null); + + setSelectedDeliveryProduct(currentProducts ?? values ?? null); + if (id) { setStep(2); } From 1dafb0d365c02ec65237285b11a208cbc203f833 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Apr 2026 11:09:04 +0700 Subject: [PATCH 06/10] fix: adjust DeliveryProductToFieldValues and mergeSOwithDO return values --- .../marketing/form/MarketingForm.schema.ts | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index 3d7eac5c..e34fab4e 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -144,15 +144,15 @@ export const DeliveryProductToFieldValues = ( delivery: BaseDeliveryOrder ): DeliveryOrderProductFormValues[] => { const data = delivery.deliveries.map((item) => { - const soId = salesOrders.find( + const salesOrder = salesOrders.find( (so) => so.product_warehouse.id === item.product_warehouse.id - )?.id; + ); const warehouseOption = { value: item.product_warehouse.warehouse.id, label: item.product_warehouse.warehouse.name, }; return { - id: soId, + id: salesOrder?.id, unit_price: item.unit_price, total_weight: item.total_weight, qty: item.qty, @@ -161,9 +161,21 @@ export const DeliveryProductToFieldValues = ( vehicle_number: item.vehicle_number, delivery_date: formatDate(delivery.delivery_date, 'yyyy-MM-DD'), do_number: delivery.do_number, - marketing_product_id: soId, + marketing_product_id: salesOrder?.id, + marketing_type: salesOrder?.marketing_type + ? { + value: salesOrder?.marketing_type, + label: formatTitleCase(salesOrder?.marketing_type), + } + : null, + convertion_unit: salesOrder?.convertion_unit + ? { + value: salesOrder?.convertion_unit.toLowerCase(), + label: formatTitleCase(salesOrder?.convertion_unit), + } + : null, marketing_product: { - id: soId, + id: salesOrder?.id, vehicle_number: item.vehicle_number, warehouse_id: item.product_warehouse.warehouse.id, warehouse: warehouseOption, @@ -183,6 +195,7 @@ export const DeliveryProductToFieldValues = ( }, } as DeliveryOrderProductFormValues; }); + return data; }; export const mergeSOwithDO = ( @@ -214,30 +227,30 @@ export const mergeSOwithDO = ( delivery_date: delivery?.delivery_date || undefined, do_number: delivery?.do_number || undefined, vehicle_number: delivery?.vehicle_number || so.vehicle_number, - unit_price: autofill ? salesOrderUnitPrice : delivery?.unit_price, - total_weight: autofill ? so.total_weight : delivery?.total_weight, - qty: autofill ? so.qty : delivery?.qty, - avg_weight: autofill ? so.avg_weight : delivery?.avg_weight, - total_price: autofill ? so.total_price : delivery?.total_price, + unit_price: autofill ? delivery?.unit_price : salesOrderUnitPrice, + total_weight: autofill ? delivery?.total_weight : so.total_weight, + qty: autofill ? delivery?.qty : so.qty, + avg_weight: autofill ? delivery?.avg_weight : so.avg_weight, + total_price: autofill ? delivery?.total_price : so.total_price, marketing_product: so, // jika ada, override - uom: autofill ? so.uom : delivery?.uom, + uom: autofill ? delivery?.uom : so.uom, weight_per_convertion: autofill - ? so.weight_per_convertion - : delivery?.weight_per_convertion, + ? delivery?.weight_per_convertion + : so.weight_per_convertion, price_per_convertion: autofill - ? so.price_per_convertion - : delivery?.price_per_convertion, + ? delivery?.price_per_convertion + : so.price_per_convertion, convertion_unit: autofill - ? so.convertion_unit - : delivery?.convertion_unit, - marketing_type: autofill ? so.marketing_type : delivery?.marketing_type, - total_peti: autofill ? so.total_peti : delivery?.total_peti, - price_per_qty: autofill ? salesOrderPricePerQty : delivery?.price_per_qty, - sisa_berat: autofill ? so.sisa_berat : delivery?.sisa_berat, + ? delivery?.convertion_unit + : so.convertion_unit, + marketing_type: autofill ? delivery?.marketing_type : so.marketing_type, + total_peti: autofill ? delivery?.total_peti : so.total_peti, + price_per_qty: autofill ? delivery?.price_per_qty : salesOrderPricePerQty, + sisa_berat: autofill ? delivery?.sisa_berat : so.sisa_berat, price_sisa_berat: autofill - ? so.price_sisa_berat - : delivery?.price_sisa_berat, - week: autofill ? so.week : delivery?.week, + ? delivery?.price_sisa_berat + : so.price_sisa_berat, + week: autofill ? delivery?.week : so.week, } as DeliveryOrderProductFormValues; }); }; From 986f429ea934fe637dd00ae230a7501575075103 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Apr 2026 11:21:20 +0700 Subject: [PATCH 07/10] fix: add edit button to delivery item --- .../table-view/DeliveryOrderProductTable.tsx | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index 5051d631..a1e0c9de 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -5,8 +5,9 @@ import { Icon } from '@iconify/react'; import { useRef, useMemo } from 'react'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport'; -import { Marketing, BaseDelivery } from '@/types/api/marketing/marketing'; +import { Marketing } from '@/types/api/marketing/marketing'; import { Warehouse } from '@/types/api/master-data/warehouse'; +import { DeliveryProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm.schema'; type DeliveryOrderProductTableProps = { data: DeliveryOrderProductFormValues[]; @@ -55,14 +56,17 @@ const DeliveryOrderProductTable = ({ const deliveryItems = useMemo(() => { if (!hasDeliveryOrder) return []; + return ( marketing?.delivery_order?.flatMap((doItem) => - doItem.deliveries.map((delivery) => ({ - ...delivery, - do_number: doItem.do_number, - delivery_date: doItem.delivery_date, - warehouse: doItem.warehouse, - })) + DeliveryProductToFieldValues(marketing?.sales_order, doItem).map( + (delivery) => ({ + ...delivery, + do_number: doItem.do_number, + delivery_date: doItem.delivery_date, + warehouse: doItem.warehouse, + }) + ) ) ?? [] ); }, [marketing?.delivery_order, hasDeliveryOrder]); @@ -212,7 +216,7 @@ const DeliveryOrderProductTable = ({ }; const renderDeliveryOrderContent = ( - item: BaseDelivery & { + item: DeliveryOrderProductFormValues & { do_number: string; delivery_date: string; warehouse: Warehouse; @@ -231,6 +235,24 @@ const DeliveryOrderProductTable = ({
Value
+ {formType !== 'success' && + (formType === 'add_delivery' || + formType === 'edit_delivery' || + formType === 'detail') && ( +
+ +
+ )}
@@ -242,14 +264,14 @@ const DeliveryOrderProductTable = ({ Produk - {item.product_warehouse?.product?.name} + {item.marketing_product?.product_warehouse_data?.product.name} Qty {item.qty - ? `${formatNumber(item.qty)} ${item.product_warehouse?.product?.uom?.name ?? ''}` + ? `${formatNumber(Number(item.qty))} ${item.marketing_product?.product_warehouse_data?.product.uom.name ?? ''}` : '-'} @@ -272,13 +294,13 @@ const DeliveryOrderProductTable = ({ Total Harga Satuan - {formatCurrency(item.unit_price)} + {formatCurrency(Number(item.unit_price))} Total Penjualan - {formatCurrency(item.total_price)} + {formatCurrency(Number(item.total_price))} @@ -334,7 +356,9 @@ const DeliveryOrderProductTable = ({
{hasDeliveryOrder ? deliveryItems.map((item, index) => ( -
+
{formType === 'success' ? (
) : ( Date: Thu, 9 Apr 2026 14:14:50 +0700 Subject: [PATCH 08/10] feat: add export button --- .../production/recording/RecordingTable.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index cea30502..8104d162 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -48,6 +48,7 @@ import { useUiStore } from '@/stores/ui/ui.store'; import { usePathname } from 'next/navigation'; import { Color } from '@/types/theme'; import ButtonFilter from '@/components/helper/ButtonFilter'; +import Dropdown from '@/components/Dropdown'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { @@ -352,6 +353,9 @@ const RecordingTable = () => { const [isRejectLoading, setIsRejectLoading] = useState(false); const [, setApprovalNotes] = useState(''); + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); + const singleDeleteModal = useModal(); const approveModal = useModal(); const rejectModal = useModal(); @@ -686,6 +690,14 @@ const RecordingTable = () => { }); }, [selectedRowIds, recordings, isRecordingApproved]); + const exportToExcelHandler = async () => { + setIsLoadingExportingToExcel(true); + + await RecordingApi.exportToExcel(getTableFilterQueryString()); + + setIsLoadingExportingToExcel(false); + }; + useEffect(() => { if (isResponseSuccess(recordings) && recordings.data) { const newSelection: Record = {}; @@ -1313,6 +1325,50 @@ const RecordingTable = () => { onClick={handleFilterModalOpen} className='px-3 py-2.5' /> + + + + Export +
+ +
+ + } + className={{ + content: + 'mt-1 rounded-xl border border-base-content/5 shadow-sm overflow-hidden', + }} + > + +
From e50f4dbddba0fcc1a70b79ab8ea532d33f167769 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Apr 2026 14:15:12 +0700 Subject: [PATCH 09/10] feat: add exportToExcel method to RecordingService --- src/services/api/production.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/services/api/production.ts b/src/services/api/production.ts index d481081d..1f2a0373 100644 --- a/src/services/api/production.ts +++ b/src/services/api/production.ts @@ -12,6 +12,8 @@ import { NextDayRecording, } from '@/types/api/production/recording'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { httpClient } from '@/services/http/client'; +import { formatDate } from '@/lib/helper'; export const ProjectFlockKandangApi = new BaseApiService< ProjectFlockKandang, @@ -88,6 +90,30 @@ export class RecordingService extends BaseApiService< } ); } + + async exportToExcel(initialQueryString: string) { + const params = new URLSearchParams(initialQueryString); + + params.set('export', 'excel'); + + const queryString = `?${params.toString()}`; + + const res = await httpClient(`${this.basePath}${queryString}`, { + method: 'GET', + responseType: 'blob', + }); + + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + + const fileName = `recording-${formatDate(Date.now(), 'DD-MM-YYYY')}.xlsx`; + link.setAttribute('download', fileName); + + document.body.appendChild(link); + link.click(); + link.remove(); + } } export const RecordingApi = new RecordingService('/production/recordings'); From 62d250109b7ca451b3f6a53178b6e4a03cf30d7e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 9 Apr 2026 14:15:29 +0700 Subject: [PATCH 10/10] feat: add responseType to axios config --- src/services/http/base.ts | 7 +++++++ src/services/http/client.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/src/services/http/base.ts b/src/services/http/base.ts index 83bb5ee6..267e7622 100644 --- a/src/services/http/base.ts +++ b/src/services/http/base.ts @@ -9,6 +9,13 @@ export type RequestOptions = { auth?: AuthMode; // 'cookie' | 'bearer' | 'none' token?: string; // required if auth === 'bearer' timeoutMs?: number; + responseType?: + | 'arraybuffer' + | 'blob' + | 'document' + | 'json' + | 'text' + | 'stream'; }; export class HttpError extends Error { diff --git a/src/services/http/client.ts b/src/services/http/client.ts index 42e71978..c70a82ea 100644 --- a/src/services/http/client.ts +++ b/src/services/http/client.ts @@ -40,6 +40,7 @@ export async function httpClient( data: opts.body, timeout: opts.timeoutMs ?? 10_000, withCredentials: isCookieAuth && !isBearerAuth, + responseType: opts.responseType, headers: { ...(isFormData ? {} : { 'Content-Type': 'application/json' }), ...(opts.headers ?? {}),