From 760e9ccd89c3ed69c810c1c74475daefe323353f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 4 Feb 2026 14:13:59 +0700 Subject: [PATCH 01/25] refactor(FE): Add invoice download and PO table columns --- .../pages/purchase/PurchaseTable.tsx | 99 ++++++++++++++++++- .../purchase/order/PurchaseOrderInvoice.tsx | 30 +++++- src/types/api/purchase/purchase.d.ts | 18 +++- 3 files changed, 137 insertions(+), 10 deletions(-) diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 11543eca..732541eb 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -17,6 +17,7 @@ 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'; @@ -158,6 +159,27 @@ 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[] = [ { @@ -168,10 +190,66 @@ const PurchaseTable = () => { }, }, { - accessorKey: 'supplier', + 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', header: 'Vendor', cell: (props) => props.row.original.supplier.name, }, + { + accessorKey: 'requester_name', + header: 'Nama Pengaju', + cell: (props) => props.row.original.requester_name || '-', + }, + { + accessorKey: 'products.name', + header: 'Produk', + cell: (props) => { + const products = props.row.original.products; + if (!products || products.length === 0) return '-'; + return ( +
    + {products.map((product, index) => ( +
  • {product.name}
  • + ))} +
+ ); + }, + }, + { + accessorKey: 'location.name', + header: 'Lokasi', + cell: (props) => props.row.original.location?.name || '-', + }, { accessorKey: 'po_date', header: 'Tgl. PO', @@ -180,6 +258,14 @@ const PurchaseTable = () => { ? formatDate(props.row.original.po_date, 'DD MMM YYYY') : '-', }, + { + accessorKey: 'due_date', + header: 'Jatuh Tempo', + cell: (props) => + props.row.original.due_date + ? formatDate(props.row.original.due_date, 'DD MMM YYYY') + : '-', + }, { header: 'Aging', cell: (props) => { @@ -231,7 +317,7 @@ const PurchaseTable = () => { color={statusColor} text={statusText} className={{ - badge: 'whitespace-nowrap', + badge: 'whitespace-nowrap max-w-max w-fit', }} /> ); @@ -409,6 +495,15 @@ const PurchaseTable = () => { onClick: confirmationModalDeleteClickHandler, }} /> + + {invoicePurchaseData && ( +
+ +
+ )} ); }; diff --git a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx index aed154d0..4ad093e1 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 } from 'react'; +import { useMemo, useState, useEffect, useCallback, useRef } from 'react'; import { Page, Text, @@ -235,11 +235,16 @@ const pdfStyles = StyleSheet.create({ interface PurchaseOrderInvoiceProps { data?: Purchase; className?: string; + triggerDownloadOnMount?: boolean; } -const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { +const PurchaseOrderInvoice = ({ + data, + triggerDownloadOnMount, +}: PurchaseOrderInvoiceProps) => { const [, setIsGeneratingPDF] = useState(false); const purchaseData = data; + const hasDownloadedRef = useRef(false); const grandTotal = useMemo(() => { return ( @@ -250,7 +255,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { ); }, [purchaseData?.items]); - const handleDownloadPDF = async () => { + const handleDownloadPDF = useCallback(async () => { if (!purchaseData) { toast.error('No purchase order data available'); return; @@ -510,7 +515,20 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { } 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 ( @@ -520,6 +538,10 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { ); } + if (triggerDownloadOnMount) { + return null; + } + return purchaseData?.po_number && purchaseData.po_number !== 'Belum dibuat' ? ( -
+
= @@ -59,6 +76,23 @@ export const SalesOrderProductSchema: Yup.ObjectSchema Number(value.toFixed(2)); const roundPrice = (value: number) => Math.round(value); @@ -41,6 +51,7 @@ const SalesOrderProductForm = ({ const [currentInput, setCurrentInput] = useState(''); const [selectedProductWarehouse, setSelectedProductWarehouse] = useState(null); + const [hasSisaBerat, setHasSisaBerat] = useState(false); // ============ Formik ============ const formik = useFormik({ @@ -57,6 +68,13 @@ const SalesOrderProductForm = ({ avg_weight: initialValues?.avg_weight || '', total_price: initialValues?.total_price || '', uom: initialValues?.uom || '', + weight_per_convertion: + initialValues?.weight_per_convertion != null + ? Number(initialValues.weight_per_convertion) + : null, + convertion_unit: initialValues?.convertion_unit || null, + marketing_type: initialValues?.marketing_type || null, + total_peti: initialValues?.total_peti ?? null, }, validationSchema: SalesOrderProductSchema, onSubmit: async (values) => { @@ -76,6 +94,14 @@ const SalesOrderProductForm = ({ loadMore: loadMoreKandang, } = useSelect(WarehouseApi.basePath, 'id', 'name'); + // Options Weeks dari minggu 1 - 22 + const optionsWeeks = useMemo(() => { + return Array.from({ length: 22 }, (_, i) => ({ + value: i + 1, + label: `Weeks ${i + 1}`, + })); + }, []); + const { options: warehouseSourceOptions, rawData: warehouseSourceRawData, @@ -89,6 +115,7 @@ const SalesOrderProductForm = ({ '', { warehouse_id: formik.values.kandang_id?.toString() ?? '', + type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '', } ); @@ -279,174 +306,450 @@ const SalesOrderProductForm = ({ onSubmit={handleFormSubmit} onReset={handleResetForm} > - {formErrorMessage && ( -
setFormErrorMessage('')} className='my-3 w-full'> - - {formErrorMessage ? formErrorMessage : ''} - -
- )} - - - - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('qty')} - isError={formik.touched.qty && Boolean(formik.errors.qty)} - errorMessage={formik.errors.qty} - placeholder='Masukan Kuantitas' - endAdornment={ -
- - {selectedProductWarehouse?.product?.uom?.name} +
+ {formErrorMessage && ( +
setFormErrorMessage('')} + className='my-3 w-full' + > + + {formErrorMessage ? formErrorMessage : ''} + +
+ )} + {/* Nomor Polisi */} + + + {/* Gudang */} + + + {/* Kategori */} + { + formik.setFieldValue('marketing_type', val); + warehouseChangeHandler(null); + formik.setFieldValue('product_warehouse', null); + formik.setFieldValue('product_warehouse_id', null); + formik.setFieldValue('convertion_unit', null); + formik.setFieldValue('weight_per_convertion', null); + formik.setFieldValue('total_peti', null); + }} + isClearable + placeholder='Pilih Kategori' + /> + + {/* Produk */} + + + {/* Konversi Satuan Telur */} + {formik.values.marketing_type && + formik.values.marketing_type.value.toLowerCase() === 'telur' && + (!formik.values.convertion_unit || + formik.values.convertion_unit.value.toLowerCase() !== 'peti') && ( + formik.setFieldValue('convertion_unit', val)} + isClearable + placeholder='Pilih Konversi Satuan' + /> + )} + {formik.values.convertion_unit && + formik.values.convertion_unit.value === 'peti' && ( +
+ +
+
+ +
+ {formatTitleCase( + formik.values.convertion_unit.value + )} + +
+
+
+ } + className={{ + wrapper: 'relative', + content: + 'rounded-xl mt-1 border border-base-content/5 shadow-sm overflow-hidden min-w-68.5 sm:min-w-103.25 w-full', + }} + > +
    + {MARKETING_CONVERTION_UNIT_OPTIONS.map((option) => ( +
  • + +
  • + ))} +
+ +
+ { + formik.setFieldValue( + 'weight_per_convertion', + Number(e.target.value) + ); + setCurrentInput(e.target.name); + }} + /> +
+
+ )} + + {/* Konversi Satuan Weeks Pullet */} + {formik.values.marketing_type?.value.toLowerCase() === + 'ayam_pullet' && ( + { + formik.setFieldValue('weeks', val); + }} + placeholder='Pilih Weeks' + /> + )} + + {/* Total Peti */} + {formik.values.convertion_unit?.value === 'peti' && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('total_peti')} + isError={ + formik.touched.total_peti && Boolean(formik.errors.total_peti) + } + errorMessage={formik.errors.total_peti} + placeholder='Masukan Total Peti' + endAdornment={ +
+ Kg +
+ } + bottomLabel={`1 ${formik.values.convertion_unit?.value} = ${formik.values.weight_per_convertion ?? 0} Kg`} + /> + )} + + {/* Avg. Bobot */} + {formik.values.marketing_type?.value.toLowerCase() === 'trading' || + (formik.values.convertion_unit?.value.toLowerCase() !== 'peti' && + formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('avg_weight')} + isError={ + formik.touched.avg_weight && + Boolean(formik.errors.avg_weight) + } + errorMessage={formik.errors.avg_weight} + placeholder='Masukan Bobot Rata-rata' + /> + ))} + + {/* Total Bobot */} + {formik.values.marketing_type?.value.toLowerCase() !== 'trading' && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('total_weight')} + isError={ + formik.touched.total_weight && + Boolean(formik.errors.total_weight) + } + errorMessage={formik.errors.total_weight} + placeholder='Masukan Total Bobot' + /> + )} + + {/* Kuantitas */} + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('qty')} + isError={formik.touched.qty && Boolean(formik.errors.qty)} + errorMessage={formik.errors.qty} + placeholder='Masukan Kuantitas' + endAdornment={ +
+ + {selectedProductWarehouse?.product?.uom?.name} + +
+ } + bottomLabel={ + isResponseSuccess(warehouseSourceRawData) && + formik.values.product_warehouse_id + ? `Stok tersedia: ${formatNumber( + warehouseSourceRawData?.data?.find( + (item) => item.id === formik.values.product_warehouse_id + )?.quantity ?? 0 + )} ${selectedProductWarehouse?.product?.uom?.name}` + : '' + } + /> + + {/* Harga per convertion unit */} + {(formik.values.convertion_unit?.value.toLowerCase() === 'peti' || + formik.values.convertion_unit?.value.toLowerCase() === 'kg') && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('price_per_convertion')} + isError={ + formik.touched.price_per_convertion && + Boolean(formik.errors.price_per_convertion) + } + errorMessage={formik.errors.price_per_convertion} + placeholder='Masukan Harga Satuan' + /> + )} + + {/* Harga Satuan per Uom Produk Warehouse */} + {formik.values.convertion_unit?.value.toLowerCase() !== 'peti' && + formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('unit_price')} + isError={ + formik.touched.unit_price && Boolean(formik.errors.unit_price) + } + errorMessage={formik.errors.unit_price} + placeholder='Masukan Harga Satuan' + /> + )} + + {/* Sisa kg diluar peti */} + {formik.values.convertion_unit?.value.toLowerCase() === 'peti' && ( +
+
+ { + setHasSisaBerat(!hasSisaBerat); + }} + onBlur={() => handleBlurField('sisa_berat')} + className='toggle toggle-primary rounded-full before:rounded-full before:bg-base-content/50 border-base-content/50 checked:border-primary checked:bg-primary checked:before:bg-base-100' + /> + +
+ + Jika ada, masukan berat di luar peti
- } - bottomLabel={ - isResponseSuccess(warehouseSourceRawData) && - formik.values.product_warehouse_id - ? `Stok tersedia: ${formatNumber( - warehouseSourceRawData?.data?.find( - (item) => item.id === formik.values.product_warehouse_id - )?.quantity ?? 0 - )} ${selectedProductWarehouse?.product?.uom?.name}` - : '' - } - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('unit_price')} - isError={ - formik.touched.unit_price && Boolean(formik.errors.unit_price) - } - errorMessage={formik.errors.unit_price} - placeholder='Masukan Harga Satuan' - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('avg_weight')} - isError={ - formik.touched.avg_weight && Boolean(formik.errors.avg_weight) - } - errorMessage={formik.errors.avg_weight} - placeholder='Masukan Bobot Rata-rata' - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('total_weight')} - isError={ - formik.touched.total_weight && Boolean(formik.errors.total_weight) - } - errorMessage={formik.errors.total_weight} - placeholder='Masukan Total Bobot' - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('total_price')} - isError={ - formik.touched.total_price && Boolean(formik.errors.total_price) - } - errorMessage={formik.errors.total_price} - placeholder='Masukan Total Penjualan' - /> + )} -
- + {hasSisaBerat && ( + <> + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('sisa_berat')} + isError={ + formik.touched.sisa_berat && Boolean(formik.errors.sisa_berat) + } + errorMessage={formik.errors.sisa_berat} + placeholder='Masukan Sisa Berat' + /> + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('price_sisa_berat')} + isError={ + formik.touched.price_sisa_berat && + Boolean(formik.errors.price_sisa_berat) + } + errorMessage={formik.errors.price_sisa_berat} + placeholder='Masukan Harga Sisa Berat' + /> + + )} + + {/* Total Penjualan */} + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('total_price')} + isError={ + formik.touched.total_price && Boolean(formik.errors.total_price) + } + errorMessage={formik.errors.total_price} + placeholder='Masukan Total Penjualan' + /> + + {formErrorList.length > 0 && ( +
+ +
+ )}
-
+
)} {/* Konversi Satuan Weeks Pullet */} - {formik.values.marketing_type?.value.toLowerCase() === + {/* {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( { formik.setFieldValue('weeks', val); }} placeholder='Pilih Weeks' /> - )} + )} */} {/* Total Peti */} - {formik.values.convertion_unit?.value === 'peti' && ( + {formik.values.convertion_unit?.value.toLowerCase() === 'peti' && ( { formik.handleChange(e); @@ -429,7 +489,7 @@ const SalesOrderProductForm = ({ Kg
} - bottomLabel={`1 ${formik.values.convertion_unit?.value} = ${formik.values.weight_per_convertion ?? 0} Kg`} + bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`} /> )} @@ -586,12 +646,9 @@ const SalesOrderProductForm = ({
{ - setHasSisaBerat(!hasSisaBerat); - }} - onBlur={() => handleBlurField('sisa_berat')} + onChange={() => handleSisaBeratToggle(!hasSisaBerat)} className='toggle toggle-primary rounded-full before:rounded-full before:bg-base-content/50 border-base-content/50 checked:border-primary checked:bg-primary checked:before:bg-base-100' />
() + .nullable() + .optional() + .notRequired(), product_warehouse_id: Yup.number() .min(1, 'Produk wajib diisi!') .required('Produk wajib diisi!'), 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 63a88c04..c718c40c 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -179,14 +179,18 @@ const SalesOrderProductForm = ({ ); setSelectedProductWarehouse(productWarehouse || null); formik.setFieldValue('qty', productWarehouse?.quantity); + formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || ''); handleBlurField('qty'); } else { formik.setFieldValue('qty', ''); + formik.setFieldValue('uom', ''); } }; const handleResetForm = () => { setFormErrorMessage(''); + setHasSisaBerat(false); + setSelectedProductWarehouse(null); formik.resetForm({ values: { vehicle_number: '', @@ -569,11 +573,13 @@ const SalesOrderProductForm = ({ errorMessage={formik.errors.qty} placeholder='Masukan Kuantitas' endAdornment={ -
- - {selectedProductWarehouse?.product?.uom?.name} - -
+ formik.values.uom ? ( +
+ + {formik.values.uom} + +
+ ) : undefined } bottomLabel={ isResponseSuccess(warehouseSourceRawData) && @@ -582,7 +588,7 @@ const SalesOrderProductForm = ({ warehouseSourceRawData?.data?.find( (item) => item.id === formik.values.product_warehouse_id )?.quantity ?? 0 - )} ${selectedProductWarehouse?.product?.uom?.name}` + )} ${formik.values.uom}` : '' } /> From 4d7bd5213e3c517f66a4e020cf504f4c643159ad Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 5 Feb 2026 09:58:14 +0700 Subject: [PATCH 08/25] fix(FE): fixing delivery order ui --- .../marketing/add/delivery-orders/layout.tsx | 11 - .../marketing/add/delivery-orders/page.tsx | 54 -- src/app/marketing/add/sales-orders/page.tsx | 11 - .../detail/delivery-orders/edit/layout.tsx | 11 - .../detail/delivery-orders/edit/page.tsx | 62 -- src/app/marketing/detail/layout.tsx | 11 - src/app/marketing/detail/page.tsx | 49 - .../detail/sales-orders/edit/layout.tsx | 11 - .../detail/sales-orders/edit/page.tsx | 52 - .../marketing/DeliveryOrderFormModal.tsx | 65 +- .../pages/marketing/MarketingDetail.tsx | 572 ----------- .../marketing/detail/MarketingDetail.tsx | 572 ----------- .../marketing/form/MarketingForm.schema.ts | 18 + .../pages/marketing/form/MarketingForm.tsx | 872 ----------------- .../DeliverOrderProduct.schema.ts | 61 ++ .../delivery-order/DeliverOrderProduct.tsx | 908 ++++++++++++------ 16 files changed, 729 insertions(+), 2611 deletions(-) delete mode 100644 src/app/marketing/add/delivery-orders/layout.tsx delete mode 100644 src/app/marketing/add/delivery-orders/page.tsx delete mode 100644 src/app/marketing/add/sales-orders/page.tsx delete mode 100644 src/app/marketing/detail/delivery-orders/edit/layout.tsx delete mode 100644 src/app/marketing/detail/delivery-orders/edit/page.tsx delete mode 100644 src/app/marketing/detail/layout.tsx delete mode 100644 src/app/marketing/detail/page.tsx delete mode 100644 src/app/marketing/detail/sales-orders/edit/layout.tsx delete mode 100644 src/app/marketing/detail/sales-orders/edit/page.tsx delete mode 100644 src/components/pages/marketing/MarketingDetail.tsx delete mode 100644 src/components/pages/marketing/detail/MarketingDetail.tsx delete mode 100644 src/components/pages/marketing/form/MarketingForm.tsx diff --git a/src/app/marketing/add/delivery-orders/layout.tsx b/src/app/marketing/add/delivery-orders/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/marketing/add/delivery-orders/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/app/marketing/add/delivery-orders/page.tsx b/src/app/marketing/add/delivery-orders/page.tsx deleted file mode 100644 index 4d92acda..00000000 --- a/src/app/marketing/add/delivery-orders/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client'; - -import MarketingForm from '@/components/pages/marketing/form/MarketingForm'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { MarketingApi } from '@/services/api/marketing/marketing'; -import { useRouter, useSearchParams } from 'next/navigation'; -import toast from 'react-hot-toast'; -import useSWR from 'swr'; - -const EditMarketingDelivery = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const soId = searchParams.get('marketingId'); - - const { - data: marketing, - isLoading: isLoading, - mutate: refreshMarketing, - } = useSWR(`get-so-${soId}`, () => - MarketingApi.getSingle(soId ? parseInt(soId) : 0) - ); - - if (!soId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoading && (!marketing || isResponseError(marketing))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoading && } - {!isLoading && isResponseSuccess(marketing) && ( - { - refreshMarketing(); - }} - /> - )} -
- ); -}; -export default EditMarketingDelivery; diff --git a/src/app/marketing/add/sales-orders/page.tsx b/src/app/marketing/add/sales-orders/page.tsx deleted file mode 100644 index 9e33d304..00000000 --- a/src/app/marketing/add/sales-orders/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import MarketingForm from '@/components/pages/marketing/form/MarketingForm'; - -const AddSalesOrder = () => { - return ( -
- -
- ); -}; - -export default AddSalesOrder; diff --git a/src/app/marketing/detail/delivery-orders/edit/layout.tsx b/src/app/marketing/detail/delivery-orders/edit/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/marketing/detail/delivery-orders/edit/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/app/marketing/detail/delivery-orders/edit/page.tsx b/src/app/marketing/detail/delivery-orders/edit/page.tsx deleted file mode 100644 index 32625026..00000000 --- a/src/app/marketing/detail/delivery-orders/edit/page.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client'; - -import MarketingForm from '@/components/pages/marketing/form/MarketingForm'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { MarketingApi } from '@/services/api/marketing/marketing'; -import { useRouter, useSearchParams } from 'next/navigation'; -import toast from 'react-hot-toast'; -import useSWR from 'swr'; - -const EditMarketingDelivery = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const soId = searchParams.get('marketingId'); - - const { - data: marketing, - isLoading: isLoading, - mutate: refreshMarketing, - } = useSWR(`get-so-${soId}`, () => - MarketingApi.getSingle(soId ? parseInt(soId) : 0) - ); - - if (!soId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoading && (!marketing || isResponseError(marketing))) { - router.replace('/404'); - return; - } - - if ( - isResponseSuccess(marketing) && - marketing.data.latest_approval.step_number != 3 - ) { - toast.error('Data Marketing perlu dilakukan approval terlebih dahulu!'); - router.back(); - } - - return ( -
- {isLoading && } - {!isLoading && isResponseSuccess(marketing) && ( - { - refreshMarketing(); - }} - /> - )} -
- ); -}; -export default EditMarketingDelivery; diff --git a/src/app/marketing/detail/layout.tsx b/src/app/marketing/detail/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/marketing/detail/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/app/marketing/detail/page.tsx b/src/app/marketing/detail/page.tsx deleted file mode 100644 index 902251e8..00000000 --- a/src/app/marketing/detail/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -'use client'; - -import MarketingDetail from '@/components/pages/marketing/detail/MarketingDetail'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { MarketingApi } from '@/services/api/marketing/marketing'; -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; - -const DetailMarketing = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const soId = searchParams.get('marketingId'); - - const { - data: marketing, - isLoading: isLoading, - mutate: refreshMarketing, - } = useSWR(soId, (id: number) => MarketingApi.getSingle(id)); - - if (!soId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoading && (!marketing || isResponseError(marketing))) { - router.replace('/404'); - return; - } - - return ( -
- {isLoading && } - {!isLoading && isResponseSuccess(marketing) && ( - - )} -
- ); -}; - -export default DetailMarketing; diff --git a/src/app/marketing/detail/sales-orders/edit/layout.tsx b/src/app/marketing/detail/sales-orders/edit/layout.tsx deleted file mode 100644 index 7220dfa1..00000000 --- a/src/app/marketing/detail/sales-orders/edit/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SuspenseHelper from '@/components/helper/SuspenseHelper'; - -const Layout = ({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) => { - return {children}; -}; - -export default Layout; diff --git a/src/app/marketing/detail/sales-orders/edit/page.tsx b/src/app/marketing/detail/sales-orders/edit/page.tsx deleted file mode 100644 index 19a098c5..00000000 --- a/src/app/marketing/detail/sales-orders/edit/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import MarketingForm from '@/components/pages/marketing/form/MarketingForm'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { MarketingApi } from '@/services/api/marketing/marketing'; -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; - -const EditSalesOrder = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const soId = searchParams.get('marketingId'); - - const { - data: marketing, - isLoading: isLoading, - mutate: refreshMarketing, - } = useSWR(`get-so-${soId}`, () => - MarketingApi.getSingle(soId ? parseInt(soId) : 0) - ); - - if (!soId) { - router.back(); - - return ( -
- -
- ); - } - - if (!isLoading && (!marketing || isResponseError(marketing))) { - router.replace('/404'); - return; - } - return ( -
- {isLoading && } - {!isLoading && isResponseSuccess(marketing) && ( - { - refreshMarketing(); - }} - /> - )} -
- ); -}; -export default EditSalesOrder; diff --git a/src/components/pages/marketing/DeliveryOrderFormModal.tsx b/src/components/pages/marketing/DeliveryOrderFormModal.tsx index 2cf4ef5c..7c953fe8 100644 --- a/src/components/pages/marketing/DeliveryOrderFormModal.tsx +++ b/src/components/pages/marketing/DeliveryOrderFormModal.tsx @@ -1,16 +1,10 @@ 'use client'; import AlertErrorList from '@/components/helper/form/FormErrors'; -import { useSelect, OptionType } from '@/components/input/SelectInput'; +import { OptionType } from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import { - mergeSOwithDO, - SalesProductToFieldValues, - DeliveryProductToFieldValues, -} from '@/components/pages/marketing/form/MarketingForm'; import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; -import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { @@ -18,16 +12,13 @@ import { MarketingApi, SalesOrderApi, } from '@/services/api/marketing/marketing'; -import { CustomerApi } from '@/services/api/master-data'; -import { UserApi } from '@/services/api/user'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; -import { BaseApproval, CreatedUser } from '@/types/api/api-general'; +import { BaseApproval } from '@/types/api/api-general'; import { CreateDeliveryOrderPayload, Marketing, UpdateDeliveryOrderPayload, } from '@/types/api/marketing/marketing'; -import { Customer } from '@/types/api/master-data/customer'; import { useFormik } from 'formik'; import { Icon } from '@iconify/react'; import { useRouter, useSearchParams } from 'next/navigation'; @@ -47,6 +38,9 @@ import { DeliveryOrderSchema, getFilledMarketingFormInitialValues, SalesOrderFormValues, + mergeSOwithDO, + SalesProductToFieldValues, + DeliveryProductToFieldValues, } from '@/components/pages/marketing/form/MarketingForm.schema'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import RequirePermission from '@/components/helper/RequirePermission'; @@ -116,13 +110,6 @@ const DeliveryOrderFormModal = ({ const formRef = useRef(null); const textareaRef = useRef(null); - const [grandTotal, setGrandTotal] = useState( - isResponseSuccess(marketing) && - marketing?.data.sales_order - ?.map((item) => item.total_price) - .reduce((a, b) => a + b, 0) - ); - const [formErrorMessage, setFormErrorMessage] = useState(null); const [isLoading, setIsLoading] = useState(false); const [selectedDeliveryProduct, setSelectedDeliveryProduct] = @@ -505,6 +492,14 @@ const DeliveryOrderFormModal = ({ formik.setFieldValue('delivery_order', deliveryOrderValues); }, [deliveryOrderValues]); + const grandTotal = useMemo(() => { + return deliveryOrderValues.reduce( + (total, product) => + total + parseFloat((product.total_price as string) || '0'), + 0 + ); + }, [deliveryOrderValues]); + return ( <> )} -
-

+
+

{step == 2 && 'Ubah '} Informasi{' '} {step == 2 ? 'Delivery' : 'Produk'}

{step === 1 && ( - +
+ +
)} {step === 2 && ( void; -}) => { - const router = useRouter(); - const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( - 'APPROVED' - ); - const [grandTotal, setGrandTotal] = useState( - initialValues?.sales_order - ?.map((item) => item.total_price) - .reduce((a, b) => a + b, 0) - ); - const [isLoading, setIsLoading] = useState(false); - - const deleteModal = useModal(); - const confirmationModal = useModal(); - const deliveryModal = useModal(); - const { - approvals, - isLoading: isLoadingApproval, - refresh: refreshApproval, - } = useApprovalSteps({ - latestApproval: initialValues?.latest_approval, - approvalLines: MARKETING_APPROVAL_LINE, - moduleName: 'MARKETINGS', - moduleId: initialValues?.id as number as unknown as string, - }); - - const approveClickHandler = () => { - setApprovalAction('APPROVED'); - confirmationModal.openModal(); - }; - - const rejectClickHandler = () => { - setApprovalAction('REJECTED'); - confirmationModal.openModal(); - }; - - const deleteClickHandler = () => { - deleteModal.openModal(); - }; - - const confirmationModalDeleteClickHandler = async () => { - setIsLoading(true); - const res = await MarketingApi.delete(initialValues?.id as number); - deleteModal.closeModal(); - router.push('/marketing'); - toast.success(res?.message as string); - setIsLoading(false); - }; - - const confirmationModalApproveClickHandler = async (notes: string) => { - setIsLoading(true); - const res = await SalesOrderApi.singleApproval( - initialValues?.id as number, - approvalAction, - notes - ); - setIsLoading(false); - confirmationModal.closeModal(); - toast.success(res?.message as string); - refresh?.(); - refreshApproval?.(); - }; - - const confirmationModalDeliveryClickHandler = async (notes: string) => { - setIsLoading(true); - const res = await SalesOrderApi.delivery( - initialValues?.id as number, - notes - ); - setIsLoading(false); - deliveryModal.closeModal(); - toast.success(res?.message as string); - refresh?.(); - refreshApproval?.(); - router.push( - `/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}` - ); - }; - - const approval = initialValues?.latest_approval; - const isRejected = approval?.action == 'REJECTED'; - const isApproved = approval?.action == 'APPROVED'; - - return ( - <> -
- 2 ? 'Delivery Order' : 'Sales Order'}`} - backUrl='/marketing' - /> - {!isLoadingApproval && approvals && ( - - )} -
- {initialValues?.latest_approval?.step_number == 1 && ( - <> - - - - - - - - - )} - {initialValues?.latest_approval?.step_number != 1 && ( - <> - - - - - )} -
- - -
- - - - - - - - {Number(initialValues?.latest_approval?.step_number) > 2 && ( - - - - - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {Number(initialValues?.latest_approval?.step_number) > 2 && ( - - - - - - )} - -
- No. Sales Order - : - {initialValues?.so_number} -
- No. Delivery Order - : - {initialValues?.delivery_order - ?.map((item) => item.do_number) - .join(', ')} -
Nama Pelanggan:{initialValues?.customer?.name}
Status: - - - {isRejected - ? 'Ditolak' - : formatTitleCase(approval?.step_name || '')} - -
Tanggal Penjualan:{formatDate(initialValues?.so_date, 'DD MMM yyyy')}
Total Penjualan:{formatCurrency(grandTotal as number)}
Catatan:{initialValues?.notes ?? '-'}
Dokumen Penjualan: - -
Dokumen Pengiriman: - {initialValues?.delivery_order?.map((item, index) => ( - - ))} -
-
-
- {initialValues?.sales_order && ( - - - data={initialValues?.sales_order} - columns={[ - { - header: 'Kandang', - accessorFn(row) { - return row.product_warehouse.warehouse.name; - }, - }, - { - header: 'Produk', - accessorFn(row) { - return row.product_warehouse.product.name; - }, - }, - { - header: 'Harga Satuan (Rp)', - accessorFn(row) { - return formatCurrency(row.unit_price); - }, - }, - { - header: 'Total Bobot (Kg)', - accessorFn(row) { - return formatNumber(row.total_weight); - }, - }, - { - header: 'Kuantitas', - accessorFn(row) { - return formatNumber(row.qty); - }, - }, - { - header: 'Avg. Bobot (Kg)', - accessorFn(row) { - return formatNumber(row.avg_weight); - }, - }, - { - header: 'Total Penjualan (Rp)', - accessorFn(row) { - return formatCurrency(row.total_price); - }, - }, - ]} - className={{ - containerClassName: cn({ - 'mb-20': - initialValues?.sales_order && - initialValues?.sales_order?.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', - paginationClassName: 'hidden', - }} - /> - - )} - {initialValues?.delivery_order && ( - - {initialValues?.delivery_order.map((delivery, index) => { - return ( -
- -
-
- Nomor DO : {delivery.do_number} -
-
- - data={delivery.deliveries} - columns={[ - { - header: 'Tanggal Pengiriman', - accessorFn() { - return formatDate( - delivery.delivery_date, - 'DD MMM yyyy' - ); - }, - }, - { - header: 'No. Polisi', - accessorFn(row) { - return formatVechicleNumber(row.vehicle_number); - }, - }, - { - header: 'Kandang', - accessorFn(row) { - return row.product_warehouse.warehouse.name; - }, - }, - { - header: 'Produk', - accessorFn(row) { - return row.product_warehouse.product.name; - }, - }, - { - header: 'Harga Satuan (Rp)', - accessorFn(row) { - return formatCurrency(row.unit_price); - }, - }, - { - header: 'Total Bobot (Kg)', - accessorFn(row) { - return formatNumber(row.total_weight); - }, - }, - { - header: 'Kuantitas', - accessorFn(row) { - return formatNumber(row.qty); - }, - }, - { - header: 'Avg. Bobot (Kg)', - accessorFn(row) { - return formatNumber(row.avg_weight); - }, - }, - { - header: 'Total Penjualan (Rp)', - accessorFn(row) { - return formatCurrency(row.total_price); - }, - }, - ]} - className={{ - containerClassName: cn({ - 'mb-20': - initialValues?.sales_order && - initialValues?.sales_order?.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', - paginationClassName: 'hidden', - }} - /> -
-
- -
-
- ); - })} -
- )} -
- {initialValues?.latest_approval?.step_number != 3 && ( - <> - - - - - )} - - - -
-
- - - - - ); -}; - -export default MarketingDetail; diff --git a/src/components/pages/marketing/detail/MarketingDetail.tsx b/src/components/pages/marketing/detail/MarketingDetail.tsx deleted file mode 100644 index f8f7d269..00000000 --- a/src/components/pages/marketing/detail/MarketingDetail.tsx +++ /dev/null @@ -1,572 +0,0 @@ -'use client'; - -import Button from '@/components/Button'; -import Card from '@/components/Card'; -import { FormHeader } from '@/components/helper/form/FormHeader'; -import { useModal } from '@/components/Modal'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; -import ApprovalSteps, { - useApprovalSteps, -} from '@/components/pages/ApprovalSteps'; -import Table from '@/components/Table'; -import { MARKETING_APPROVAL_LINE } from '@/config/approval-line'; -import { - cn, - formatCurrency, - formatDate, - formatNumber, - formatTitleCase, - formatVechicleNumber, -} from '@/lib/helper'; -import { - MarketingApi, - SalesOrderApi, -} from '@/services/api/marketing/marketing'; -import { - BaseDelivery, - BaseSalesOrder, - Marketing, -} from '@/types/api/marketing/marketing'; -import { Icon } from '@iconify/react'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import toast from 'react-hot-toast'; -import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport'; -import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport'; -import RequirePermission from '@/components/helper/RequirePermission'; -import Badge from '@/components/Badge'; - -const MarketingDetail = ({ - initialValues, - refresh, -}: { - initialValues?: Marketing; - refresh?: () => void; -}) => { - const router = useRouter(); - const [approvalAction, setApprovalAction] = useState<'APPROVED' | 'REJECTED'>( - 'APPROVED' - ); - const [grandTotal, setGrandTotal] = useState( - initialValues?.sales_order - ?.map((item) => item.total_price) - .reduce((a, b) => a + b, 0) - ); - const [isLoading, setIsLoading] = useState(false); - - const deleteModal = useModal(); - const confirmationModal = useModal(); - const deliveryModal = useModal(); - const { - approvals, - isLoading: isLoadingApproval, - refresh: refreshApproval, - } = useApprovalSteps({ - latestApproval: initialValues?.latest_approval, - approvalLines: MARKETING_APPROVAL_LINE, - moduleName: 'MARKETINGS', - moduleId: initialValues?.id as number as unknown as string, - }); - - const approveClickHandler = () => { - setApprovalAction('APPROVED'); - confirmationModal.openModal(); - }; - - const rejectClickHandler = () => { - setApprovalAction('REJECTED'); - confirmationModal.openModal(); - }; - - const deleteClickHandler = () => { - deleteModal.openModal(); - }; - - const confirmationModalDeleteClickHandler = async () => { - setIsLoading(true); - const res = await MarketingApi.delete(initialValues?.id as number); - deleteModal.closeModal(); - router.push('/marketing'); - toast.success(res?.message as string); - setIsLoading(false); - }; - - const confirmationModalApproveClickHandler = async (notes: string) => { - setIsLoading(true); - const res = await SalesOrderApi.singleApproval( - initialValues?.id as number, - approvalAction, - notes - ); - setIsLoading(false); - confirmationModal.closeModal(); - toast.success(res?.message as string); - refresh?.(); - refreshApproval?.(); - }; - - const confirmationModalDeliveryClickHandler = async (notes: string) => { - setIsLoading(true); - const res = await SalesOrderApi.delivery( - initialValues?.id as number, - notes - ); - setIsLoading(false); - deliveryModal.closeModal(); - toast.success(res?.message as string); - refresh?.(); - refreshApproval?.(); - router.push( - `/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}` - ); - }; - - const approval = initialValues?.latest_approval; - const isRejected = approval?.action == 'REJECTED'; - const isApproved = approval?.action == 'APPROVED'; - - return ( - <> -
- 2 ? 'Delivery Order' : 'Sales Order'}`} - backUrl='/marketing' - /> - {!isLoadingApproval && approvals && ( - - )} -
- {initialValues?.latest_approval?.step_number == 1 && ( - <> - - - - - - - - - )} - {initialValues?.latest_approval?.step_number != 1 && ( - <> - - - - - )} -
- - -
- - - - - - - - {Number(initialValues?.latest_approval?.step_number) > 2 && ( - - - - - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {Number(initialValues?.latest_approval?.step_number) > 2 && ( - - - - - - )} - -
- No. Sales Order - : - {initialValues?.so_number} -
- No. Delivery Order - : - {initialValues?.delivery_order - ?.map((item) => item.do_number) - .join(', ')} -
Nama Pelanggan:{initialValues?.customer?.name}
Status: - - - {isRejected - ? 'Ditolak' - : formatTitleCase(approval?.step_name || '')} - -
Tanggal Penjualan:{formatDate(initialValues?.so_date, 'DD MMM yyyy')}
Total Penjualan:{formatCurrency(grandTotal as number)}
Catatan:{initialValues?.notes ?? '-'}
Dokumen Penjualan: - -
Dokumen Pengiriman: - {initialValues?.delivery_order?.map((item, index) => ( - - ))} -
-
-
- {initialValues?.sales_order && ( - - - data={initialValues?.sales_order} - columns={[ - { - header: 'Kandang', - accessorFn(row) { - return row.product_warehouse.warehouse.name; - }, - }, - { - header: 'Produk', - accessorFn(row) { - return row.product_warehouse.product.name; - }, - }, - { - header: 'Harga Satuan (Rp)', - accessorFn(row) { - return formatCurrency(row.unit_price); - }, - }, - { - header: 'Total Bobot (Kg)', - accessorFn(row) { - return formatNumber(row.total_weight); - }, - }, - { - header: 'Kuantitas', - accessorFn(row) { - return formatNumber(row.qty); - }, - }, - { - header: 'Avg. Bobot (Kg)', - accessorFn(row) { - return formatNumber(row.avg_weight); - }, - }, - { - header: 'Total Penjualan (Rp)', - accessorFn(row) { - return formatCurrency(row.total_price); - }, - }, - ]} - className={{ - containerClassName: cn({ - 'mb-20': - initialValues?.sales_order && - initialValues?.sales_order?.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', - paginationClassName: 'hidden', - }} - /> - - )} - {initialValues?.delivery_order && ( - - {initialValues?.delivery_order.map((delivery, index) => { - return ( -
- -
-
- Nomor DO : {delivery.do_number} -
-
- - data={delivery.deliveries} - columns={[ - { - header: 'Tanggal Pengiriman', - accessorFn() { - return formatDate( - delivery.delivery_date, - 'DD MMM yyyy' - ); - }, - }, - { - header: 'No. Polisi', - accessorFn(row) { - return formatVechicleNumber(row.vehicle_number); - }, - }, - { - header: 'Kandang', - accessorFn(row) { - return row.product_warehouse.warehouse.name; - }, - }, - { - header: 'Produk', - accessorFn(row) { - return row.product_warehouse.product.name; - }, - }, - { - header: 'Harga Satuan (Rp)', - accessorFn(row) { - return formatCurrency(row.unit_price); - }, - }, - { - header: 'Total Bobot (Kg)', - accessorFn(row) { - return formatNumber(row.total_weight); - }, - }, - { - header: 'Kuantitas', - accessorFn(row) { - return formatNumber(row.qty); - }, - }, - { - header: 'Avg. Bobot (Kg)', - accessorFn(row) { - return formatNumber(row.avg_weight); - }, - }, - { - header: 'Total Penjualan (Rp)', - accessorFn(row) { - return formatCurrency(row.total_price); - }, - }, - ]} - className={{ - containerClassName: cn({ - 'mb-20': - initialValues?.sales_order && - initialValues?.sales_order?.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', - paginationClassName: 'hidden', - }} - /> -
-
- -
-
- ); - })} -
- )} -
- {initialValues?.latest_approval?.step_number != 3 && ( - <> - - - - - )} - - - -
-
- - - - - ); -}; - -export default MarketingDetail; diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index 4d86a2e7..0215217f 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -203,6 +203,24 @@ export const mergeSOwithDO = ( avg_weight: autofill ? so.avg_weight : delivery?.avg_weight, total_price: autofill ? so.total_price : delivery?.total_price, marketing_product: so, // jika ada, override + uom: autofill ? so.uom : delivery?.uom, + weight_per_convertion: autofill + ? so.weight_per_convertion + : delivery?.weight_per_convertion, + price_per_convertion: autofill + ? so.price_per_convertion + : delivery?.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 ? so.price_per_qty : delivery?.price_per_qty, + sisa_berat: autofill ? so.sisa_berat : delivery?.sisa_berat, + price_sisa_berat: autofill + ? so.price_sisa_berat + : delivery?.price_sisa_berat, + week: autofill ? so.week : delivery?.week, } as DeliveryOrderProductFormValues; }); }; diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx deleted file mode 100644 index d2d26bc2..00000000 --- a/src/components/pages/marketing/form/MarketingForm.tsx +++ /dev/null @@ -1,872 +0,0 @@ -'use client'; - -import Button from '@/components/Button'; -import Card from '@/components/Card'; -import { FormHeader } from '@/components/helper/form/FormHeader'; -import DateInput from '@/components/input/DateInput'; -import SelectInput, { - OptionType, - useSelect, -} from '@/components/input/SelectInput'; -import Modal, { useModal } from '@/components/Modal'; -import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; -import { - BaseDeliveryOrder, - BaseSalesOrder, - CreateDeliveryOrderPayload, - CreateSalesOrderPayload, - CreateSalesOrderProductPayload, - Marketing, - UpdateDeliveryOrderPayload, - UpdateSalesOrderPayload, -} from '@/types/api/marketing/marketing'; -import { Icon } from '@iconify/react'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { Customer } from '@/types/api/master-data/customer'; -import { CustomerApi } from '@/services/api/master-data'; -import { useFormik } from 'formik'; -import { - DeliveryOrderFormValues, - DeliveryOrderSchema, - SalesOrderFormValues, - SalesOrderSchema, -} from '@/components/pages/marketing/form/MarketingForm.schema'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { - DeliveryOrderApi, - MarketingApi, - SalesOrderApi, -} from '@/services/api/marketing/marketing'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import toast from 'react-hot-toast'; -import { useRouter } from 'next/navigation'; -import DebouncedTextArea from '@/components/input/DebouncedTextArea'; -import SalesOrderProductForm from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm'; -import DeliveryOrderProductTable from '@/components/pages/marketing/form/table-view/DeliveryOrderProductTable'; -import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct'; -import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; -import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; -import RequirePermission from '@/components/helper/RequirePermission'; -import AlertErrorList from '@/components/helper/form/FormErrors'; -import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; -import { CreatedUser } from '@/types/api/api-general'; -import { UserApi } from '@/services/api/user'; - -const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm); -const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable); -const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm); - -// ================== EXTERNAL HELPER FUNCTION ================== -export interface ProductCalculationFields { - qty: string | number | undefined; - unit_price: string | number | undefined; - total_price: string | number | undefined; - avg_weight: string | number | undefined; - total_weight: string | number | undefined; -} - -export const SalesProductToFieldValues = ( - product: BaseSalesOrder -): SalesOrderProductFormValues => { - return { - id: product.id, - vehicle_number: product.vehicle_number, - kandang_id: product.product_warehouse.warehouse.id, - kandang: { - value: product.product_warehouse.warehouse.id, - label: product.product_warehouse.warehouse.name, - }, - product_warehouse: { - value: product.product_warehouse.id, - label: product.product_warehouse.product.name, - }, - product_warehouse_id: product.product_warehouse.id, - unit_price: product.unit_price, - total_weight: product.total_weight, - qty: product.qty, - avg_weight: product.avg_weight, - total_price: product.total_price, - }; -}; -export const DeliveryProductToFieldValues = ( - salesOrders: BaseSalesOrder[], - delivery: BaseDeliveryOrder -): DeliveryOrderProductFormValues[] => { - const data = delivery.deliveries.map((item) => { - const soId = salesOrders.find( - (so) => so.product_warehouse.id === item.product_warehouse.id - )?.id; - return { - id: soId, - unit_price: item.unit_price, - total_weight: item.total_weight, - qty: item.qty, - avg_weight: item.avg_weight, - total_price: item.total_price, - 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: soId, - vehicle_number: item.vehicle_number, - kandang_id: item.product_warehouse.warehouse.id, - kandang: { - value: item.product_warehouse.warehouse.id, - label: item.product_warehouse.warehouse.name, - }, - product_warehouse: { - value: item.product_warehouse.id, - label: item.product_warehouse.product.name, - }, - product_warehouse_id: item.product_warehouse.id, - unit_price: item.unit_price, - total_weight: item.total_weight, - qty: item.qty, - avg_weight: item.avg_weight, - total_price: item.total_price, - }, - } as DeliveryOrderProductFormValues; - }); - return data; -}; -export const mergeSOwithDO = ( - salesOrders: SalesOrderProductFormValues[], - deliveryOrders: DeliveryOrderProductFormValues[], - autofill?: boolean -): DeliveryOrderProductFormValues[] => { - return salesOrders.map((so) => { - const delivery = deliveryOrders.find( - (d) => d?.marketing_product_id === so.id - ); - - return { - ...so, // nilai dasar dari sales order - marketing_product_id: so.id, - 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, - 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, - marketing_product: so, // jika ada, override - } as DeliveryOrderProductFormValues; - }); -}; -export const recalculate = ( - field: string, - values: ProductCalculationFields -) => { - const { qty, unit_price, total_price, avg_weight, total_weight } = values; - const result: Partial = {}; - if (field == 'unit_price' || field == 'total_price' || field == 'qty') { - if (qty && unit_price && (field == 'unit_price' || field == 'qty')) { - result.total_price = Number(qty) * Number(unit_price); - } else if (qty && total_price && field == 'total_price') { - result.unit_price = Number(total_price) / Number(qty); - } - } - if (field == 'avg_weight' || field == 'total_weight' || field == 'qty') { - if (qty && avg_weight && (field == 'avg_weight' || field == 'qty')) { - result.total_weight = Number(qty) * Number(avg_weight); - } else if (qty && total_weight && field == 'total_weight') { - result.avg_weight = Number(total_weight) / Number(qty); - } - } - return result; -}; -export const getSubmitField = (values: ProductCalculationFields) => { - const { qty, unit_price, total_price, avg_weight, total_weight } = values; - - // Harga logic - if (qty && unit_price && !total_price) { - return 'unit_price'; - } - if (qty && total_price && !unit_price) { - return 'total_price'; - } - - // Bobot logic - if (qty && avg_weight && !total_weight) { - return 'avg_weight'; - } - if (qty && total_weight && !avg_weight) { - return 'total_weight'; - } - - // Tidak ada yang perlu dihitung - return ''; -}; - -const MarketingForm = ({ - formType = 'add', - initialValues, - afterSubmit, -}: { - formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver'; - initialValues?: Marketing; - afterSubmit?: () => void; -}) => { - const router = useRouter(); - const deleteModal = useModal(); - - const [isLoading, setIsLoading] = useState(false); - const [selectedMarketingProduct, setSelectedMarketingProduct] = - useState(null); - const [selectedDeliveryProduct, setSelectedDeliveryProduct] = - useState(null); - const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>( - 'add' - ); - const [deliveryOrderValues, setDeliveryOrderValues] = useState< - DeliveryOrderProductFormValues[] - >( - mergeSOwithDO( - initialValues?.sales_order?.map(SalesProductToFieldValues) ?? [], - initialValues?.delivery_order?.flatMap((delivery) => - DeliveryProductToFieldValues(initialValues.sales_order, delivery) - ) ?? [] - ) - ); - - // ================== REPEATER ================== - const addSOModal = useModal(); - const addDOModal = useModal(); - const [rowSOSelection, setRowSOSelection] = useState>( - {} - ); - const selectedRowSOIds = Object.keys(rowSOSelection).map((item) => - parseInt(item) - ); - - // ================== FETCH OPTIONS ================== - const { - options: customerOptions, - isLoadingOptions: isLoadingCustomerOptions, - setInputValue: setInputCustomerValue, - loadMore: loadMoreCustomer, - } = useSelect(CustomerApi.basePath, 'id', 'name'); - const { - options: salesOptions, - isLoadingOptions: isLoadingSalesOptions, - setInputValue: setInputSalesValue, - loadMore: loadMoreSales, - } = useSelect(UserApi.basePath, 'id', 'name'); - - // ================== SETUP FORMIK ================== - const formikInitialValues = useMemo< - SalesOrderFormValues & DeliveryOrderFormValues - >(() => { - return { - so_date: initialValues?.so_date || undefined, - notes: initialValues?.notes || undefined, - customer_id: initialValues?.customer?.id || undefined, - sales_person_id: initialValues?.sales_person?.id || 1, - sales_person: initialValues?.sales_person - ? { - value: initialValues.sales_person.id, - label: initialValues.sales_person.name, - } - : null, - customer: initialValues?.customer - ? { - value: initialValues.customer.id, - label: initialValues.customer.name, - } - : null, - sales_order: - initialValues?.sales_order?.map((product) => - SalesProductToFieldValues(product) - ) ?? [], - delivery_order: mergeSOwithDO( - initialValues?.sales_order?.map(SalesProductToFieldValues) ?? [], - initialValues?.delivery_order?.flatMap((delivery) => - DeliveryProductToFieldValues(initialValues.sales_order, delivery) - ) ?? [] - ), - }; - }, [initialValues]); - const formik = useFormik({ - enableReinitialize: true, - initialValues: formikInitialValues, - validationSchema: - formType == 'add_deliver' || formType == 'edit_deliver' - ? DeliveryOrderSchema - : SalesOrderSchema, - validateOnMount: true, - onSubmit: async (values) => { - const payload = - formType != 'add_deliver' && formType != 'edit_deliver' - ? ({ - customer_id: values.customer_id as number, - sales_person_id: values.sales_person_id as number, - date: formatDate(values.so_date as string, 'yyyy-MM-DD'), - notes: values.notes as string, - marketing_products: values.sales_order.map((product) => { - return { - vehicle_number: product.vehicle_number as string, - kandang_id: product.kandang_id as number, - product_warehouse_id: product.product_warehouse_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), - } as CreateSalesOrderProductPayload; - }), - } as CreateSalesOrderPayload) - : ({ - marketing_id: initialValues?.id as number, - delivery_products: values.delivery_order - .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); - switch (formType) { - case 'add': - await createMarketingHandler(payload as CreateSalesOrderPayload); - break; - case 'edit': - await updateMarketingHandler(payload as UpdateSalesOrderPayload); - break; - case 'add_deliver': - await createDeliveryHandler(payload as CreateDeliveryOrderPayload); - break; - case 'edit_deliver': - await updateDeliveryHandler(payload as UpdateDeliveryOrderPayload); - break; - default: - break; - } - afterSubmit?.(); - }, - }); - - const memoSalesOrder = formik.values.sales_order; - - // ================== FORM REPEATER HANDLER ================== - const createMarketingHandler = async (values: CreateSalesOrderPayload) => { - setIsLoading(true); - const createMarketingRes = await SalesOrderApi.create(values); - if (isResponseSuccess(createMarketingRes)) { - toast.success(createMarketingRes?.message as string); - router.push('/marketing'); - } - if (isResponseError(createMarketingRes)) { - toast.error(createMarketingRes?.message as string); - } - setIsLoading(false); - }; - const updateMarketingHandler = async (values: UpdateSalesOrderPayload) => { - setIsLoading(true); - const updateMarketingRes = await SalesOrderApi.update( - initialValues?.id as number, - values - ); - if (isResponseSuccess(updateMarketingRes)) { - toast.success(updateMarketingRes?.message as string); - router.push(`/marketing/detail?marketingId=${initialValues?.id}`); - } - if (isResponseError(updateMarketingRes)) { - toast.error(updateMarketingRes?.message as string); - } - setIsLoading(false); - }; - const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => { - setIsLoading(true); - const createDeliveryRes = await DeliveryOrderApi.create(values); - if (isResponseSuccess(createDeliveryRes)) { - toast.success(createDeliveryRes?.message as string); - setDeliveryOrderValues( - createDeliveryRes.data?.delivery_order?.flatMap((delivery) => - DeliveryProductToFieldValues( - createDeliveryRes.data?.sales_order, - delivery - ) - ) ?? [] - ); - router.push(`/marketing/detail?marketingId=${initialValues?.id}`); - } - if (isResponseError(createDeliveryRes)) { - toast.error(createDeliveryRes?.message as string); - } - setIsLoading(false); - }; - const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => { - setIsLoading(true); - const updateDeliveryRes = await DeliveryOrderApi.update( - initialValues?.id as number, - values - ); - if (isResponseSuccess(updateDeliveryRes)) { - toast.success(updateDeliveryRes?.message as string); - setDeliveryOrderValues( - mergeSOwithDO( - formik.values.sales_order, - updateDeliveryRes.data?.delivery_order?.flatMap((delivery) => - DeliveryProductToFieldValues( - updateDeliveryRes.data?.sales_order, - delivery - ) - ) ?? [] - ) - ); - router.push(`/marketing/detail?marketingId=${initialValues?.id}`); - } - if (isResponseError(updateDeliveryRes)) { - toast.error(updateDeliveryRes?.message as string); - } - setIsLoading(false); - }; - - // ================== MARKETING HANDLER ================== - const deleteMarketingHandler = async () => { - setIsLoading(true); - const deleteMarketingRes = await MarketingApi.delete( - initialValues?.id as number - ); - if (isResponseSuccess(deleteMarketingRes)) { - toast.success(deleteMarketingRes?.message as string); - } - if (isResponseError(deleteMarketingRes)) { - toast.error(deleteMarketingRes?.message as string); - } - setIsLoading(false); - deleteModal.closeModal(); - router.push('/marketing'); - }; - const handleChangeCustomer = useCallback( - (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('customer_id', (val as OptionType)?.value); - formik.setFieldValue('customer', val as OptionType); - }, - [] - ); - const handleChangeSalesPerson = useCallback( - (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('sales_person_id', (val as OptionType)?.value); - formik.setFieldValue('sales_person', val as OptionType); - }, - [] - ); - const handleDelete = useCallback(() => { - deleteModal.openModal(); - }, [deleteModal]); - - // ================== SALES ORDER HANDLER ================== - const handleDeleteSO = useCallback( - (id: number) => { - const currentProducts = formik.values.sales_order; - formik.setFieldValue( - 'sales_order', - currentProducts.filter((p) => p.id != id) - ); - }, - [memoSalesOrder] - ); - const handleEditSO = useCallback( - (id: number) => { - const currentProducts = formik.values.sales_order; - const selectedProduct = currentProducts.find((p) => p.id == id); - setSelectedMarketingProduct(selectedProduct ?? null); - addSOModal.openModal(); - }, - [memoSalesOrder] - ); - const handleBulkDeleteSO = useCallback(() => { - const currentProducts = formik.values.sales_order; - formik.setFieldValue( - 'sales_order', - currentProducts.filter( - (product) => !selectedRowSOIds.includes(product.id ?? -1) - ) - ); - setRowSOSelection({}); - }, [selectedRowSOIds, memoSalesOrder]); - const handleAddSOClick = useCallback(() => { - setSelectedMarketingProduct(null); - addSOModal.openModal(); - }, [addSOModal]); - const handleAddSubmitSO = useCallback( - async (values: SalesOrderProductFormValues, id?: number) => { - const currentProducts = formik.values.sales_order; - - const newValues = { - ...values, - id: values.id ?? Date.now(), - }; - - let updatedProducts = []; - - if (id) { - // Overwrite - updatedProducts = currentProducts.map((item) => - item.id === id ? newValues : item - ); - } else { - // Add new item - updatedProducts = [...currentProducts, newValues]; - } - - formik.setFieldValue('sales_order', updatedProducts); - - addSOModal.closeModal(); - }, - [addSOModal, memoSalesOrder] - ); - - // ================== DELIVERY ORDER HANDLER ================== - const handleEditDO = useCallback( - (id: number, values?: DeliveryOrderProductFormValues) => { - setDeliveryFormState('edit'); - const currentProducts = formik.values.delivery_order.find( - (product) => product.id == id - ); - setSelectedDeliveryProduct(values ?? currentProducts ?? null); - addDOModal.openModal(); - }, - [addDOModal] - ); - const handleAddDOClick = useCallback(() => { - setDeliveryFormState('add'); - setSelectedDeliveryProduct(null); - addDOModal.openModal(); - }, [addDOModal]); - const handleAddSubmitDO = useCallback( - async (values: DeliveryOrderProductFormValues) => { - const newValues = { - ...values, - id: values.id ?? Date.now(), - }; - - setDeliveryOrderValues((prev) => [...prev, newValues]); - addDOModal.closeModal(); - setSelectedDeliveryProduct(null); - }, - [addDOModal] - ); - const handleUpdateDO = useCallback( - async (id: number, values: DeliveryOrderProductFormValues) => { - setDeliveryOrderValues((prev) => - prev.map((product) => - product.id === id ? { ...product, ...values } : product - ) - ); - addDOModal.closeModal(); - setSelectedDeliveryProduct(null); - }, - [addDOModal] - ); - const handleDeleteDO = useCallback( - async (id: number) => { - setDeliveryOrderValues((prev) => - prev.map((product) => - product.id === id - ? { - ...product, - ...{ - unit_price: '', - total_weight: '', - qty: '', - avg_weight: '', - total_price: '', - delivery_date: '', - }, - } - : product - ) - ); - addDOModal.closeModal(); - setSelectedDeliveryProduct(null); - }, - [addDOModal] - ); - - useEffect(() => { - formik.setFieldValue('delivery_order', deliveryOrderValues); - }, [deliveryOrderValues, initialValues]); - - const grandTotal = useMemo(() => { - return memoSalesOrder.reduce( - (total, product) => - total + parseFloat((product.total_price as string) || '0'), - 0 - ); - }, [memoSalesOrder]); - - // ===== Formik Error List ===== - const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); - - return ( - <> - - - {/* Input Cutomer And Date */} - -
- - -
-
- - {/* Input Table Repeater Sales Order */} - - {/* */} - - - {/* Input Table Repeater Delivery Order */} - {(formType == 'add_deliver' || formType == 'edit_deliver') && - initialValues?.sales_order && - initialValues?.sales_order.length > 0 && ( - - - - )} - - {/* Input Notes */} -
-
- - -
-
- Total Penjualan - - {formatCurrency(grandTotal)}{' '} - -
-
- - - - {/* Form Actions */} -
- - -
- - - {/* Actions button */} - {formType == 'edit' && ( -
- - - -
- )} - - {/* Modals */} - -
-
-

Tambah Produk

- -
-
- -
-
-
- -
-
-

- {selectedDeliveryProduct ? 'Edit' : 'Tambah'} Pengiriman -

- -
-
- -
-
-
- - - ); -}; - -export default MarketingForm; 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 1fc4c7c0..4c20f05b 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 @@ -13,6 +13,30 @@ type DeliveryOrderProductSchemaType = { vehicle_number: string | undefined; delivery_date: string | undefined | null; do_number?: string | undefined | null; // Uncertain + uom?: string | null | undefined; + convertion_unit?: { + value: string; + label: string; + } | null; + weight_per_convertion?: number | null | undefined; + price_per_convertion?: number | null | undefined; + marketing_type?: { + value: string; + label: string; + } | null; + total_peti?: number | null | undefined; + sisa_berat?: number | null | undefined; + price_sisa_berat?: number | null | undefined; + /** Harga per butir telur untuk TELUR + QTY */ + price_per_qty?: number | null | undefined; + /** Week untuk ayam pullet */ + week?: + | { + value?: number; + label?: string; + } + | null + | undefined; }; export const DeliveryOrderProductSchema: Yup.ObjectSchema = @@ -40,6 +64,43 @@ 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!'), + otherwise: (schema) => schema.optional().notRequired(), + }), }); export type DeliveryOrderProductFormValues = Yup.InferType< 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 9e735a95..850d88d2 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { DeliveryOrderProductFormValues, DeliveryOrderProductSchema, @@ -8,10 +8,10 @@ import Alert from '@/components/Alert'; import Button from '@/components/Button'; import NumberInput from '@/components/input/NumberInput'; import PatternInput from '@/components/input/PatternInput'; -import { formatVechicleNumber } from '@/lib/helper'; +import { formatTitleCase, formatVechicleNumber } from '@/lib/helper'; import DateInput from '@/components/input/DateInput'; import { BaseSalesOrder } from '@/types/api/marketing/marketing'; -import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm'; +import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm.schema'; import * as Yup from 'yup'; import { isResponseSuccess } from '@/lib/api-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; @@ -21,9 +21,13 @@ import { ProductApi } from '@/services/api/master-data'; import StatusBadge from '@/components/helper/StatusBadge'; import SelectInputRadio from '@/components/input/SelectInputRadio'; import { OptionType } from '@/components/input/SelectInput'; - -const roundWeight = (value: number) => Number(value.toFixed(2)); -const roundPrice = (value: number) => Math.round(value); +import { + MARKETING_CONVERTION_UNIT_OPTIONS, + MARKETING_TYPE_OPTIONS, +} from '@/config/constant'; +import Dropdown from '@/components/Dropdown'; +import { Icon } from '@iconify/react'; +import { handleMarketingCalculation } from '@/lib/marketing-calculation'; const DeliveryOrderProductForm = ({ formState, @@ -49,6 +53,35 @@ const DeliveryOrderProductForm = ({ ); const [currentInput, setCurrentInput] = useState(''); + // Check jika ada sisa berat = total_weight - (weight_per_convertion * total_peti) + const initialSisaBerat = + initialValues?.total_weight && + initialValues?.weight_per_convertion && + initialValues?.total_peti + ? Number(initialValues.total_weight) - + Number(initialValues.weight_per_convertion) * + 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 initialPriceSisaBerat = + initialValues?.total_price && initialValues?.total_peti + ? Number(initialValues.total_price) - + initialPricePerConvertion * Number(initialValues.total_peti) + : 0; + + const [hasSisaBerat, setHasSisaBerat] = useState( + initialSisaBerat > 0 + ); + // ============ Fetch Data ============ const { data: productData } = useSWR( selectedProduct?.value @@ -60,6 +93,27 @@ const DeliveryOrderProductForm = ({ : undefined ); + // Options Week dari minggu 1 - 22 + const optionsWeek = useMemo(() => { + return Array.from({ length: 22 }, (_, i) => ({ + value: i + 1, + label: `Week ${i + 1}`, + })); + }, []); + + const options = exisitingValues + ?.map((item) => { + if (!Boolean(item.qty)) { + return { + value: item.id, + label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`, + } as OptionType; + } else { + return null; + } + }) + ?.filter((item) => item != null) as OptionType[]; + const salesOrder = salesOrders.find( (item) => item.id === initialValues?.marketing_product_id ); @@ -77,6 +131,19 @@ const DeliveryOrderProductForm = ({ avg_weight: initialValues?.avg_weight || undefined, total_price: initialValues?.total_price || undefined, marketing_product: initialValues?.marketing_product || undefined, + uom: initialValues?.uom || '', + weight_per_convertion: + initialValues?.weight_per_convertion != null + ? Number(initialValues.weight_per_convertion) + : null, + price_per_convertion: initialPricePerConvertion, + 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, + sisa_berat: initialSisaBerat, + price_sisa_berat: initialPriceSisaBerat, + week: initialValues?.week ?? null, }, isInitialValid: false, validationSchema: Yup.object().shape({ @@ -124,6 +191,16 @@ const DeliveryOrderProductForm = ({ avg_weight: '', total_price: '', marketing_product: undefined, + total_peti: null, + price_per_qty: null, + price_sisa_berat: null, + sisa_berat: null, + convertion_unit: null, + marketing_type: null, + weight_per_convertion: null, + price_per_convertion: null, + uom: '', + week: null, }, }); // setSelectedProduct(null); @@ -132,94 +209,34 @@ const DeliveryOrderProductForm = ({ const handleBlurField = (field: string) => { setCurrentInput(field); - const qty = Number(formik.values.qty || 0); - const avgWeight = Number(formik.values.avg_weight || 0); - const totalWeight = Number(formik.values.total_weight || 0); - const unitPrice = Number(formik.values.unit_price || 0); - const totalPrice = Number(formik.values.total_price || 0); - - if (qty <= 0) return; - - switch (field) { - // ===== SOURCE FIELDS ===== - case 'qty': { - if (avgWeight > 0) { - const tw = roundWeight(qty * avgWeight); - formik.setFieldValue('total_weight', tw); - - // Hitung total_price berdasarkan unit_price × total_weight - if (unitPrice > 0) { - formik.setFieldValue('total_price', roundPrice(unitPrice * tw)); - } - } - break; - } - - case 'avg_weight': { - if (avgWeight > 0) { - const tw = roundWeight(qty * avgWeight); - formik.setFieldValue('total_weight', tw); - - // Hitung total_price berdasarkan unit_price × total_weight - if (unitPrice > 0) { - formik.setFieldValue('total_price', roundPrice(unitPrice * tw)); - } - } - break; - } - - case 'unit_price': { - if (unitPrice > 0 && totalWeight > 0) { - // Hitung total_price berdasarkan unit_price × total_weight - formik.setFieldValue( - 'total_price', - roundPrice(unitPrice * totalWeight) - ); - } - break; - } - - // ===== TOTAL EDITABLE ===== - case 'total_weight': { - if (totalWeight > 0) { - formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty)); - - // Hitung ulang total_price berdasarkan unit_price × total_weight - if (unitPrice > 0) { - formik.setFieldValue( - 'total_price', - roundPrice(unitPrice * totalWeight) - ); - } - } - break; - } - - case 'total_price': { - if (totalPrice > 0 && totalWeight > 0) { - // Hitung unit_price berdasarkan total_price / total_weight - formik.setFieldValue( - 'unit_price', - roundPrice(totalPrice / totalWeight) - ); - } - break; - } - } + handleMarketingCalculation(field, { + values: formik.values, + setFieldValue: formik.setFieldValue, + hasSisaBerat, + }); }; - const options = exisitingValues - ?.map((item) => { - if (!Boolean(item.qty)) { - return { - value: item.id, - label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`, - } as OptionType; - } else { - return null; - } - }) - ?.filter((item) => item != null) as OptionType[]; + // Handler khusus untuk toggle sisa berat - langsung pakai nilai baru + const handleSisaBeratToggle = (newHasSisaBerat: boolean) => { + setHasSisaBerat(newHasSisaBerat); + + if (!newHasSisaBerat) { + // Ketika OFF - set nilai ke 0 dan recalculate tanpa sisa + formik.setFieldValue('sisa_berat', 0); + formik.setFieldValue('price_sisa_berat', 0); + } + + // Langsung trigger recalculation dengan hasSisaBerat yang baru + handleMarketingCalculation('total_peti', { + values: { + ...formik.values, + sisa_berat: newHasSisaBerat ? formik.values.sisa_berat : 0, + price_sisa_berat: newHasSisaBerat ? formik.values.price_sisa_berat : 0, + }, + setFieldValue: formik.setFieldValue, + hasSisaBerat: newHasSisaBerat, + }); + }; const { setValues: setFormikValues } = formik; @@ -229,9 +246,6 @@ const DeliveryOrderProductForm = ({ handleResetForm(); } else { setFormikValues(initialValues); - // const value = exisitingValues?.find( - // (item) => item.id === initialValues?.id - // ); if (initialValues?.marketing_product_id) { setSelectedProduct({ value: initialValues?.id, @@ -243,7 +257,23 @@ const DeliveryOrderProductForm = ({ }, [initialValues]); // ===== Formik Error List ===== - const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + const { formErrorList, close, handleFormSubmit } = useFormikErrorList( + formik, + { + onBeforeSubmit(e) { + e.preventDefault(); + handleBlurField(currentInput); + formik.setFieldValue( + 'uom', + isResponseSuccess(productData) ? productData?.data?.uom?.name : '' + ); + }, + } + ); + + useEffect(() => { + handleBlurField('week'); + }, [formik.values.week]); return ( <> @@ -252,214 +282,514 @@ const DeliveryOrderProductForm = ({ onSubmit={handleFormSubmit} onReset={handleResetForm} > - {formikErrorMessage && ( -
setFormErrorMessage('')} className='my-3 w-full'> - {formikErrorMessage} -
- )} - - item.id === selectedProduct?.value - )?.marketing_product?.product_warehouse?.label, - } as OptionType) - : null - } - onChange={(value) => { - const selected = value as OptionType; - setSelectedProduct(selected); +
+ {formikErrorMessage && ( +
setFormErrorMessage('')} + className='my-3 w-full' + > + {formikErrorMessage} +
+ )} + + {/* Tanggal Pengiriman */} + + + {/* No. Polisi */} + + + {/* Produk */} + item.id === selectedProduct?.value + )?.marketing_product?.product_warehouse?.label, + } as OptionType) + : null + } + onChange={(value) => { + const selected = value as OptionType; + setSelectedProduct(selected); + + const so = salesOrders?.find( + (item) => item.id === selected?.value + ); + if (!so) { + formik.setValues({ + ...formik.values, + marketing_product_id: undefined, + marketing_product: null, + qty: '', + unit_price: '', + total_price: '', + avg_weight: '', + total_weight: '', + vehicle_number: '', + }); + return; + } - const so = salesOrders?.find((item) => item.id === selected?.value); - if (!so) { formik.setValues({ ...formik.values, - marketing_product_id: undefined, - marketing_product: null, - qty: '', - unit_price: '', - total_price: '', - avg_weight: '', - total_weight: '', - vehicle_number: '', + marketing_product_id: selected.value as number, + marketing_product: SalesProductToFieldValues(so), + qty: so.qty, + unit_price: so.unit_price, + total_price: so.total_price, + avg_weight: so.avg_weight, + total_weight: so.total_weight, + vehicle_number: so.vehicle_number, }); - return; + }} + startAdornment={ + selectedProduct && ( + item.id === selectedProduct?.value + )?.marketing_product?.kandang?.label ?? '' + } + color='success' + className={{ + badge: 'whitespace-nowrap w-fit font-semibold', + }} + /> + ) } + isClearable + isError={Boolean(formik.errors.marketing_product_id)} + errorMessage={formik.errors.marketing_product_id} + required + /> - formik.setValues({ - ...formik.values, - marketing_product_id: selected.value as number, - marketing_product: SalesProductToFieldValues(so), - qty: so.qty, - unit_price: so.unit_price, - total_price: so.total_price, - avg_weight: so.avg_weight, - total_weight: so.total_weight, - vehicle_number: so.vehicle_number, - }); - }} - startAdornment={ - selectedProduct && ( - item.id === selectedProduct?.value - )?.marketing_product?.kandang?.label ?? '' - } - color='success' - className={{ - badge: 'whitespace-nowrap w-fit font-semibold', - }} + {/* Kategori */} + { + formik.setFieldValue('marketing_type', val); + }} + isClearable + placeholder='Pilih Kategori' + isDisabled + /> + + {/* Konversi Satuan Telur */} + {formik.values.marketing_type && + formik.values.marketing_type.value.toLowerCase() === 'telur' && + (!formik.values.convertion_unit || + formik.values.convertion_unit.value.toLowerCase() !== 'peti') && ( + formik.setFieldValue('convertion_unit', val)} + isClearable + placeholder='Pilih Konversi Satuan' /> - ) - } - isClearable - isError={Boolean(formik.errors.marketing_product_id)} - errorMessage={formik.errors.marketing_product_id} - required - /> + )} + {formik.values.convertion_unit && + formik.values.convertion_unit.value.toLowerCase() === 'peti' && ( +
+ +
+
+ +
+ {formatTitleCase( + formik.values.convertion_unit.value + )} + +
+
+
+ } + className={{ + wrapper: 'relative', + content: + 'rounded-xl mt-1 border border-base-content/5 shadow-sm overflow-hidden min-w-68.5 sm:min-w-103.25 w-full', + }} + > +
    + {MARKETING_CONVERTION_UNIT_OPTIONS.map((option) => ( +
  • + +
  • + ))} +
+ +
+ { + formik.setFieldValue( + 'weight_per_convertion', + Number(e.target.value) + ); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('weight_per_convertion')} + /> +
+
+ )} - - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('qty')} - isError={Boolean(formik.errors.qty)} - errorMessage={formik.errors.qty} - placeholder='Masukan Kuantitas' - endAdornment={ -
- - {isResponseSuccess(productData) - ? productData?.data?.uom.name - : ''} + {/* Konversi Satuan Week Pullet */} + {formik.values.marketing_type?.value.toLowerCase() === + 'ayam_pullet' && ( + { + formik.setFieldValue('week', val); + }} + placeholder='Pilih Week' + /> + )} + + {/* Total Peti */} + {formik.values.convertion_unit?.value.toLowerCase() === 'peti' && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('total_peti')} + isError={ + formik.touched.total_peti && Boolean(formik.errors.total_peti) + } + errorMessage={formik.errors.total_peti} + placeholder='Masukan Total Peti' + endAdornment={ +
+ Kg +
+ } + bottomLabel={`1 ${formik.values.convertion_unit?.value.toLowerCase()} = ${formik.values.weight_per_convertion ?? 0} Kg`} + /> + )} + + {/* Avg. Bobot */} + {formik.values.marketing_type?.value.toLowerCase() === 'trading' || + (formik.values.convertion_unit?.value.toLowerCase() !== 'peti' && + formik.values.convertion_unit?.value.toLowerCase() !== 'kg' && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('avg_weight')} + isError={ + formik.touched.avg_weight && + Boolean(formik.errors.avg_weight) + } + errorMessage={formik.errors.avg_weight} + placeholder='Masukan Bobot Rata-rata' + /> + ))} + + {/* Total Bobot */} + {formik.values.marketing_type?.value.toLowerCase() !== 'trading' && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('total_weight')} + isError={ + formik.touched.total_weight && + Boolean(formik.errors.total_weight) + } + errorMessage={formik.errors.total_weight} + placeholder='Masukan Total Bobot' + /> + )} + + {/* Kuantitas */} + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('qty')} + isError={Boolean(formik.errors.qty)} + errorMessage={formik.errors.qty} + placeholder='Masukan Kuantitas' + endAdornment={ +
+ + {isResponseSuccess(productData) + ? productData?.data?.uom.name + : ''} + +
+ } + bottomLabel={ + formik.values.marketing_product_id + ? 'Stok dijual: ' + + salesOrders?.find( + (item) => item.id === formik.values.marketing_product_id + )?.qty + + ' ' + + (isResponseSuccess(productData) + ? productData?.data?.uom.name + : '') + : '' + } + /> + + {/* Harga per convertion unit (PETI / KG) */} + {(formik.values.convertion_unit?.value.toLowerCase() === 'peti' || + formik.values.convertion_unit?.value.toLowerCase() === 'kg') && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('price_per_convertion')} + isError={ + formik.touched.price_per_convertion && + Boolean(formik.errors.price_per_convertion) + } + errorMessage={formik.errors.price_per_convertion} + placeholder='Masukan Harga Satuan' + /> + )} + + {/* Harga per butir untuk TELUR + QTY */} + {formik.values.marketing_type?.value.toLowerCase() === 'telur' && + formik.values.convertion_unit?.value.toLowerCase() === 'qty' && ( + { + formik.setFieldValue('price_per_qty', Number(e.target.value)); + setCurrentInput('price_per_qty'); + }} + onBlur={() => handleBlurField('price_per_qty')} + isError={ + formik.touched.price_per_qty && + 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' && ( + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('unit_price')} + isError={Boolean(formik.errors.unit_price)} + errorMessage={formik.errors.unit_price} + placeholder='Masukan Harga Satuan' + /> + )} + + {/* Sisa kg diluar peti */} + {formik.values.convertion_unit?.value.toLowerCase() === 'peti' && ( +
+
+ handleSisaBeratToggle(!hasSisaBerat)} + className='toggle toggle-primary rounded-full before:rounded-full before:bg-base-content/50 border-base-content/50 checked:border-primary checked:bg-primary checked:before:bg-base-100' + /> + +
+ + Jika ada, masukan berat di luar peti
- } - bottomLabel={ - formik.values.marketing_product_id - ? 'Stok dijual: ' + - salesOrders?.find( - (item) => item.id === formik.values.marketing_product_id - )?.qty + - ' ' + - (isResponseSuccess(productData) - ? productData?.data?.uom.name - : '') - : '' - } - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('unit_price')} - isError={Boolean(formik.errors.unit_price)} - errorMessage={formik.errors.unit_price} - placeholder='Masukan Harga Satuan' - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('avg_weight')} - isError={Boolean(formik.errors.avg_weight)} - errorMessage={formik.errors.avg_weight} - placeholder='Masukan Bobot Rata-rata' - /> - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('total_weight')} - isError={Boolean(formik.errors.total_weight)} - errorMessage={formik.errors.total_weight} - placeholder='Masukan Total Bobot' - /> + )} - { - formik.handleChange(e); - setCurrentInput(e.target.name); - }} - onBlur={() => handleBlurField('total_price')} - isError={Boolean(formik.errors.total_price)} - errorMessage={formik.errors.total_price} - placeholder='Masukan Total Penjualan' - /> + {hasSisaBerat && ( + <> + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('sisa_berat')} + isError={ + formik.touched.sisa_berat && Boolean(formik.errors.sisa_berat) + } + errorMessage={formik.errors.sisa_berat} + placeholder='Masukan Sisa Berat' + /> + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('price_sisa_berat')} + isError={ + formik.touched.price_sisa_berat && + Boolean(formik.errors.price_sisa_berat) + } + errorMessage={formik.errors.price_sisa_berat} + placeholder='Masukan Harga Sisa Berat' + /> + + )} - + {/* Total Penjualan */} + { + formik.handleChange(e); + setCurrentInput(e.target.name); + }} + onBlur={() => handleBlurField('total_price')} + isError={ + formik.touched.total_price && Boolean(formik.errors.total_price) + } + errorMessage={formik.errors.total_price} + placeholder='Masukan Total Penjualan' + /> + +
-
+
@@ -601,22 +591,34 @@ const FinanceTable = () => { isClearable />
diff --git a/src/components/pages/finance/FinanceTableFilter.schema.ts b/src/components/pages/finance/FinanceTableFilter.schema.ts new file mode 100644 index 00000000..bc1053d3 --- /dev/null +++ b/src/components/pages/finance/FinanceTableFilter.schema.ts @@ -0,0 +1,39 @@ +import { OptionType } from '@/components/input/SelectInput'; +import * as yup from 'yup'; + +export type FinanceTableFilterType = { + search: string; + transaction_types: string; + bank_ids: string; + customer_ids: string; + supplier_ids: string; + sort_by: string; + start_date: string; + end_date: string; +}; + +export const FinanceTableFilterSchema = yup.object({ + search: yup.string().optional(), + transaction_types: yup.string().optional(), + bank_ids: yup.string().optional(), + customer_ids: yup.string().optional(), + supplier_ids: yup.string().optional(), + sort_by: yup.string().optional(), + start_date: yup.string().optional(), + end_date: yup + .string() + .optional() + .test( + 'is-greater-than-start', + 'Tanggal akhir tidak boleh masa lampau', + function (value) { + const { start_date } = this.parent; + if (!start_date || !value) return true; + return new Date(value) >= new Date(start_date); + } + ), +}) as yup.ObjectSchema; + +export type FinanceTableFilterValues = yup.InferType< + typeof FinanceTableFilterSchema +>; From 4aa9d54b1e6182a5668cc939556a68a9095afc96 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 11:02:20 +0700 Subject: [PATCH 13/25] refactor(FE): Remove unused search params and yup import --- src/components/pages/finance/FinanceTable.tsx | 40 +++++++------------ .../finance/FinanceTableFilter.schema.ts | 1 - 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index aba80e4e..40091237 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -3,7 +3,6 @@ import { CellContext } from '@tanstack/react-table'; import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { useFormik } from 'formik'; -import * as yup from 'yup'; import Button from '@/components/Button'; import Card from '@/components/Card'; @@ -174,7 +173,6 @@ const FinanceTable = () => { }); // ===== State ===== - const [searchParams, setSearchParams] = useSearchParams(); const deleteModal = useModal(); const [selectedTransactionType, setSelectedTransactionType] = useState< OptionType | OptionType[] | null @@ -254,6 +252,20 @@ const FinanceTable = () => { loadMore: bankLoadMore, } = useSelect(BankApi.basePath, 'id', 'alias'); + const bankSelectOptions = useMemo(() => { + if (!isResponseSuccess(bankRawData)) return []; + + return bankOptions.map((bank) => { + const bankData = bankRawData.data.find((data) => data.id === bank?.value); + return { + label: bankData + ? `${bankData.alias} - ${bankData.account_number} - ${bankData.owner}` + : '', + value: bank?.value, + }; + }); + }, [bankOptions, bankRawData]); + // ===== Handler ===== const searchChangeHandler = (e: React.ChangeEvent) => { filterFormik.setFieldValue('search', e.target.value); @@ -311,10 +323,6 @@ const FinanceTable = () => { val ? ((val as OptionType).value as string) : '' ); }; - const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - }; const resetFilterHandler = () => { setSelectedTransactionType(null); setSelectedBank(null); @@ -454,18 +462,15 @@ const FinanceTable = () => { }, []); useEffect(() => { - // Store current path on mount previousPathRef.current = window.location.pathname; return () => { const currentPath = window.location.pathname; - // if both paths are within /finance module const isCurrentPathFinance = currentPath.includes('/finance'); const isPreviousPathFinance = previousPathRef.current?.includes('/finance'); - // reset if we outside finance module entirely if (isPreviousPathFinance && !isCurrentPathFinance) { resetSearchValue(); } @@ -558,22 +563,7 @@ const FinanceTable = () => { isMulti /> ({ - label: - bankRawData.data.find((data) => data.id === bank?.value) - ?.alias + - ' - ' + - bankRawData.data.find((data) => data.id === bank?.value) - ?.account_number + - ' - ' + - bankRawData.data.find((data) => data.id === bank?.value) - ?.owner, - value: bank?.value, - })) - : [] - } + options={bankSelectOptions} label='Bank' value={selectedBank} onChange={bankChangeHandler} diff --git a/src/components/pages/finance/FinanceTableFilter.schema.ts b/src/components/pages/finance/FinanceTableFilter.schema.ts index bc1053d3..fecfc35d 100644 --- a/src/components/pages/finance/FinanceTableFilter.schema.ts +++ b/src/components/pages/finance/FinanceTableFilter.schema.ts @@ -1,4 +1,3 @@ -import { OptionType } from '@/components/input/SelectInput'; import * as yup from 'yup'; export type FinanceTableFilterType = { From 372b439ff01cdc542f861da5c0ba52bc9a413d2d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 11:13:53 +0700 Subject: [PATCH 14/25] refactor(FE): Validate date range and show persistent toast --- src/components/pages/finance/FinanceTable.tsx | 92 +++++++++++++++++-- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 40091237..6f422753 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -189,6 +189,7 @@ const FinanceTable = () => { const [selectedSortBy, setSelectedSortBy] = useState(null); const [selectedFinance, setSelectedFinance] = useState(null); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [dateErrorShown, setDateErrorShown] = useState(false); // ===== Formik for Filter ===== const filterFormik = useFormik({ @@ -323,6 +324,70 @@ const FinanceTable = () => { val ? ((val as OptionType).value as string) : '' ); }; + + const startDateChangeHandler = (e: React.ChangeEvent) => { + const value = e.target.value; + const endDate = filterFormik.values.end_date; + + filterFormik.setFieldValue('start_date', value); + + if (value && endDate) { + const startDate = new Date(value); + const endDateObj = new Date(endDate); + + if (endDateObj < startDate) { + filterFormik.setFieldError( + 'end_date', + 'Tanggal akhir tidak boleh masa lampau' + ); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + filterFormik.setFieldError('end_date', undefined); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } + }; + + const endDateChangeHandler = (e: React.ChangeEvent) => { + const value = e.target.value; + const startDate = filterFormik.values.start_date; + + filterFormik.setFieldValue('end_date', value); + + if (value && startDate) { + const startDateObj = new Date(startDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + filterFormik.setFieldError( + 'end_date', + 'Tanggal akhir tidak boleh masa lampau' + ); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + filterFormik.setFieldError('end_date', undefined); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + const resetFilterHandler = () => { setSelectedTransactionType(null); setSelectedBank(null); @@ -461,6 +526,14 @@ const FinanceTable = () => { ]; }, []); + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + useEffect(() => { previousPathRef.current = window.location.pathname; @@ -474,8 +547,13 @@ const FinanceTable = () => { if (isPreviousPathFinance && !isCurrentPathFinance) { resetSearchValue(); } + + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } }; - }, [resetSearchValue]); + }, [resetSearchValue, dateErrorShown]); return (
@@ -584,25 +662,23 @@ const FinanceTable = () => { name='start_date' label='Periode Tanggal (Mulai)' value={filterFormik.values.start_date} - onChange={filterFormik.handleChange} + onChange={startDateChangeHandler} errorMessage={ - filterFormik.touched.start_date && filterFormik.errors.start_date - ? filterFormik.errors.start_date + filterFormik.errors.end_date + ? filterFormik.errors.end_date : undefined } - onBlur={filterFormik.handleBlur} /> Date: Thu, 5 Feb 2026 12:04:08 +0700 Subject: [PATCH 15/25] refactor(FE): Use API metadata for table pagination --- src/components/pages/marketing/MarketingTable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index e09617aa..0a35a8bc 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -623,7 +623,10 @@ const MarketingTable = () => { data={allData} columns={columns} pageSize={tableFilterState.pageSize} - page={tableFilterState.page} + page={isResponseSuccess(marketing) ? marketing?.meta?.page : 1} + totalItems={ + isResponseSuccess(marketing) ? marketing?.meta?.total_results : 0 + } isLoading={isLoadingMarketing} className={{ containerClassName: cn('p-3', { From 1af2b72beade136c4eccf69b2957528056657f03 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 12:06:04 +0700 Subject: [PATCH 16/25] refactor(FE): Prevent badge text wrapping --- src/components/pages/expense/ExpenseStatusBadge.tsx | 2 +- src/components/pages/expense/RealizationStatusBadge.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/expense/ExpenseStatusBadge.tsx b/src/components/pages/expense/ExpenseStatusBadge.tsx index eee84224..854b4d34 100644 --- a/src/components/pages/expense/ExpenseStatusBadge.tsx +++ b/src/components/pages/expense/ExpenseStatusBadge.tsx @@ -49,7 +49,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { color={expenseStatusBadgeColor} text={isLatestApprovalRejected ? 'Ditolak' : (approval?.step_name ?? '')} className={{ - badge: 'w-fit', + badge: 'whitespace-nowrap max-w-max w-fit', }} /> ); diff --git a/src/components/pages/expense/RealizationStatusBadge.tsx b/src/components/pages/expense/RealizationStatusBadge.tsx index d04d35c3..eb429473 100644 --- a/src/components/pages/expense/RealizationStatusBadge.tsx +++ b/src/components/pages/expense/RealizationStatusBadge.tsx @@ -29,7 +29,7 @@ const RealizationStatusBadge = ({ approval }: RealizationStatusBadgeProps) => { color={realizationStatusBadgeColor} text={isLatestApprovalRejected ? 'Ditolak' : realizationStatus} className={{ - badge: 'w-fit', + badge: 'whitespace-nowrap max-w-max w-fit', }} /> ); From d41600d8e229bf476d662a969f164596b1afc54a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 13:48:07 +0700 Subject: [PATCH 17/25] refactor(FE): Replace week SelectInputRadio with NumberInput --- .../delivery-order/DeliverOrderProduct.tsx | 21 ++++++++++++------- .../sales-order/SalesOrderProductForm.tsx | 21 ++++++++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) 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..f5089c84 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,24 @@ const DeliveryOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', val); + 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/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index c718c40c..ae1e4a44 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,24 @@ const SalesOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', val); + 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} /> )} From 3b221795baf19b5eb945ddc3008147b3046430a8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:08:05 +0700 Subject: [PATCH 18/25] refactor(FE): Add filter_by option to customer payment reports --- .../export/CustomerPaymentExportPDF.tsx | 3 +- .../report/finance/tab/CustomerPaymentTab.tsx | 103 ++++++++++-------- src/services/api/report/finance-report.ts | 6 +- 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index e6c5d66e..d132be9a 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -96,8 +96,7 @@ interface CustomerPaymentExportPDFParams { // sales?: string; start_date?: string; end_date?: string; - // TODO: Uncomment when BE is ready - // filter_by?: string; + filter_by?: string; }; } diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 2987455a..4f12a4b8 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -14,7 +14,6 @@ import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; import { CustomerPaymentReport, - CustomerPaymentRow, CustomerPaymentSummary, } from '@/types/api/report/customer-payment'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -53,33 +52,39 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ); // TODO: Uncomment when BE is ready // const [filterSales, setFilterSales] = useState([]); - const [filterSales, setFilterSales] = useState([]); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); const filterModal = useModal(); + const dataTypeOptions = useMemo( + () => [ + { value: 'do_date', label: 'Tanggal Jual' }, + { value: 'payment_date', label: 'Tanggal Bayar' }, + { value: 'realization_date', label: 'Tanggal Realisasi' }, + ], + [] + ); + + const [filterByType, setFilterByType] = useState<(typeof dataTypeOptions)[0]>( + dataTypeOptions[0] + ); + const { options: customerOptions, setInputValue: setCustomerInputValue, isLoadingOptions: isLoadingCustomers, loadMore: loadMoreCustomers, - hasMore: hasMoreCustomers, } = useSelect(CustomerApi.basePath, 'id', 'name', 'search'); // TODO: Uncomment when BE is ready - const { - options: salesOptions, - setInputValue: setSalesInputValue, - isLoadingOptions: isLoadingSales, - loadMore: loadMoreSales, - hasMore: hasMoreSales, - } = useSelect(UserApi.basePath, 'id', 'name', 'search'); - - const dataTypeOptions = useMemo( - () => [{ value: 'do_date', label: 'Tanggal Jual' }], - [] - ); + // const { + // options: salesOptions, + // setInputValue: setSalesInputValue, + // isLoadingOptions: isLoadingSales, + // loadMore: loadMoreSales, + // hasMore: hasMoreSales, + // } = useSelect(UserApi.basePath, 'id', 'name', 'search'); const getPaymentStatusColor = (notes: string) => { const normalizedValue = notes.toLowerCase(); @@ -125,10 +130,11 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const handleResetFilters = useCallback(() => { setIsSubmitted(false); setFilterCustomer([]); - setFilterSales([]); + // setFilterSales([]); + setFilterByType(dataTypeOptions[0]); setFilterStartDate(''); setFilterEndDate(''); - }, []); + }, [dataTypeOptions]); const handleApplyFilters = useCallback(() => { setIsSubmitted(true); @@ -157,12 +163,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // } return count; - }, [ - filterStartDate, - filterEndDate, - filterCustomer, - // filterSales, - ]); + }, [filterStartDate, filterEndDate, filterCustomer]); const hasFilters = activeFiltersCount > 0; @@ -180,7 +181,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // filterSales.length > 0 // ? filterSales.map((v) => String(v.value)).join(',') // : undefined, - // filter_by: 'do_date' as const, + filter_by: filterByType.value as + | 'do_date' + | 'payment_date' + | 'realization_date', start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, page: currentPage, @@ -193,8 +197,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ([, params]) => FinanceApi.getCustomerPaymentReport( params.customer_ids, - undefined, // TODO: Change to params.sales_id when BE is ready - undefined, // TODO: Change to params.filter_by when BE is ready + params.filter_by, params.start_date, params.end_date, params.page, @@ -224,6 +227,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // filterSales.length > 0 // ? filterSales.map((v) => String(v.value)).join(',') // : undefined, + filter_by: filterByType.value as + | 'do_date' + | 'payment_date' + | 'realization_date', start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, limit: 100, @@ -232,8 +239,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const response = await FinanceApi.getCustomerPaymentReport( params.customer_ids, - undefined, // TODO: Change to params.sales_id when BE is ready - undefined, // TODO: Change to params.filter_by when BE is ready + params.filter_by, params.start_date, params.end_date, params.page, @@ -243,7 +249,13 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { return isResponseSuccess(response) ? (response.data as unknown as CustomerPaymentReport[]) : null; - }, [filterCustomer, filterSales, filterStartDate, filterEndDate]); + }, [ + filterCustomer, + // filterSales, + filterStartDate, + filterEndDate, + filterByType, + ]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { @@ -297,8 +309,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // : undefined, start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, - // TODO: Uncomment when BE is ready - // filter_by: 'do_date' as const, + filter_by: filterByType.value as + | 'do_date' + | 'payment_date' + | 'realization_date', }, }); toast.success('PDF berhasil dibuat dan diunduh.'); @@ -406,7 +420,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { footer: () =>
Total
, }, { - id: 'do_date_or_payment_date', + id: 'trans_date', header: 'Tanggal Jual/Bayar', accessorKey: 'trans_date', enableSorting: false, @@ -864,17 +878,20 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { />
*/} - {/* TODO: Uncomment when BE is ready */} - {/*
- -
*/} +
+ { + if (val && !Array.isArray(val)) { + setFilterByType(val); + } + }} + className={{ wrapper: 'w-full' }} + /> +
{/* Action Buttons */}
diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index 1102f99c..f9c296d8 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -15,9 +15,7 @@ export class FinanceApiService extends BaseApiService< customer_ids?: string, // TODO: Uncomment when BE is ready // sales_id?: string, - // filter_by?: 'do_date', - sales_id?: string, - filter_by?: 'do_date' | undefined, + filter_by?: 'do_date' | 'payment_date' | 'realization_date', start_date?: string, end_date?: string, page?: number, @@ -31,7 +29,7 @@ export class FinanceApiService extends BaseApiService< customer_ids: customer_ids, // TODO: Uncomment when BE is ready // sales_id: sales_id, - // filter_by: filter_by, + filter_by: filter_by, start_date: start_date, end_date: end_date, page: page, From fb1b310d1d49da40d4ee779073fd02170c43ac90 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:13:13 +0700 Subject: [PATCH 19/25] refactor(FE): Replace SelectInput with SelectInputRadio --- .../report/finance/tab/CustomerPaymentTab.tsx | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 4f12a4b8..91f8df42 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -3,12 +3,13 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; -import SelectInput, { useSelect } from '@/components/input/SelectInput'; +import { useSelect } from '@/components/input/SelectInput'; import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; -import { UserApi } from '@/services/api/user'; +// import { UserApi } from '@/services/api/user'; import Table from '@/components/Table'; import { ColumnDef } from '@tanstack/react-table'; import { formatCurrency, formatDate, formatNumber, cn } from '@/lib/helper'; @@ -878,20 +879,18 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { />
*/} -
- { - if (val && !Array.isArray(val)) { - setFilterByType(val); - } - }} - className={{ wrapper: 'w-full' }} - /> -
+ { + if (val && !Array.isArray(val)) { + setFilterByType(val); + } + }} + className={{ wrapper: 'w-full' }} + /> {/* Action Buttons */}

From 92886fe5e229811c42af49cd4b36a1036e18c083 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:28:14 +0700 Subject: [PATCH 20/25] refactor(FE): Consolidate date filters into trans_date --- .../report/finance/tab/CustomerPaymentTab.tsx | 18 ++++-------------- src/services/api/report/finance-report.ts | 2 +- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 91f8df42..05aa36c0 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -60,8 +60,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const dataTypeOptions = useMemo( () => [ - { value: 'do_date', label: 'Tanggal Jual' }, - { value: 'payment_date', label: 'Tanggal Bayar' }, + { value: 'trans_date', label: 'Tanggal Jual/Bayar' }, { value: 'realization_date', label: 'Tanggal Realisasi' }, ], [] @@ -182,10 +181,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // filterSales.length > 0 // ? filterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: filterByType.value as - | 'do_date' - | 'payment_date' - | 'realization_date', + filter_by: filterByType.value as 'trans_date' | 'realization_date', start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, page: currentPage, @@ -228,10 +224,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // filterSales.length > 0 // ? filterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: filterByType.value as - | 'do_date' - | 'payment_date' - | 'realization_date', + filter_by: filterByType.value as 'trans_date' | 'realization_date', start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, limit: 100, @@ -310,10 +303,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // : undefined, start_date: filterStartDate || undefined, end_date: filterEndDate || undefined, - filter_by: filterByType.value as - | 'do_date' - | 'payment_date' - | 'realization_date', + filter_by: filterByType.value as 'trans_date' | 'realization_date', }, }); toast.success('PDF berhasil dibuat dan diunduh.'); diff --git a/src/services/api/report/finance-report.ts b/src/services/api/report/finance-report.ts index f9c296d8..95a85b85 100644 --- a/src/services/api/report/finance-report.ts +++ b/src/services/api/report/finance-report.ts @@ -15,7 +15,7 @@ export class FinanceApiService extends BaseApiService< customer_ids?: string, // TODO: Uncomment when BE is ready // sales_id?: string, - filter_by?: 'do_date' | 'payment_date' | 'realization_date', + filter_by?: 'trans_date' | 'realization_date', start_date?: string, end_date?: string, page?: number, From eb95afe9a04c89a9c099920a898539a771f36667 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:39:33 +0700 Subject: [PATCH 21/25] refactor(FE): Separate applied and modal filter state --- .../report/finance/tab/CustomerPaymentTab.tsx | 101 ++++++++++++------ 1 file changed, 71 insertions(+), 30 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 05aa36c0..7160a273 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -48,6 +48,19 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const [isSubmitted, setIsSubmitted] = useState(false); // ===== FILTER STATE ===== + const [appliedFilterCustomer, setAppliedFilterCustomer] = useState< + typeof customerOptions + >([]); + // TODO: Uncomment when BE is ready + // const [appliedFilterSales, setAppliedFilterSales] = useState< + // typeof salesOptions + // >([]); + const [appliedFilterByType, setAppliedFilterByType] = useState< + (typeof dataTypeOptions)[0] + >({ value: 'trans_date', label: 'Tanggal Jual/Bayar' }); + const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); + const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); + const [filterCustomer, setFilterCustomer] = useState( [] ); @@ -124,23 +137,47 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // ===== FILTER HANDLERS ===== const handleFilterModalOpen = useCallback(() => { + setFilterCustomer(appliedFilterCustomer); + // setFilterSales(appliedFilterSales); + setFilterByType(appliedFilterByType); + setFilterStartDate(appliedFilterStartDate); + setFilterEndDate(appliedFilterEndDate); filterModal.openModal(); - }, [filterModal]); + }, [ + filterModal, + appliedFilterCustomer, + appliedFilterByType, + appliedFilterStartDate, + appliedFilterEndDate, + ]); const handleResetFilters = useCallback(() => { setIsSubmitted(false); setFilterCustomer([]); - // setFilterSales([]); setFilterByType(dataTypeOptions[0]); setFilterStartDate(''); setFilterEndDate(''); + setAppliedFilterCustomer([]); + setAppliedFilterByType(dataTypeOptions[0]); + setAppliedFilterStartDate(''); + setAppliedFilterEndDate(''); }, [dataTypeOptions]); const handleApplyFilters = useCallback(() => { + setAppliedFilterCustomer(filterCustomer); + setAppliedFilterByType(filterByType); + setAppliedFilterStartDate(filterStartDate); + setAppliedFilterEndDate(filterEndDate); setIsSubmitted(true); setCurrentPage(1); filterModal.closeModal(); - }, [filterModal]); + }, [ + filterModal, + filterCustomer, + filterByType, + filterStartDate, + filterEndDate, + ]); // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { @@ -163,7 +200,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // } return count; - }, [filterStartDate, filterEndDate, filterCustomer]); + }, [appliedFilterStartDate, appliedFilterEndDate, appliedFilterCustomer]); const hasFilters = activeFiltersCount > 0; @@ -173,17 +210,19 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ? () => { const params = { customer_ids: - filterCustomer.length > 0 - ? filterCustomer.map((v) => String(v.value)).join(',') + appliedFilterCustomer.length > 0 + ? appliedFilterCustomer.map((v) => String(v.value)).join(',') : undefined, // TODO: Uncomment when BE is ready // sales_id: - // filterSales.length > 0 - // ? filterSales.map((v) => String(v.value)).join(',') + // appliedFilterSales.length > 0 + // ? appliedFilterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: filterByType.value as 'trans_date' | 'realization_date', - start_date: filterStartDate || undefined, - end_date: filterEndDate || undefined, + filter_by: appliedFilterByType.value as + | 'trans_date' + | 'realization_date', + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, page: currentPage, limit: pageSize, }; @@ -216,17 +255,17 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { > => { const params = { customer_ids: - filterCustomer.length > 0 - ? filterCustomer.map((v) => String(v.value)).join(',') + appliedFilterCustomer.length > 0 + ? appliedFilterCustomer.map((v) => String(v.value)).join(',') : undefined, // TODO: Uncomment when BE is ready // sales_id: - // filterSales.length > 0 - // ? filterSales.map((v) => String(v.value)).join(',') + // appliedFilterSales.length > 0 + // ? appliedFilterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: filterByType.value as 'trans_date' | 'realization_date', - start_date: filterStartDate || undefined, - end_date: filterEndDate || undefined, + filter_by: appliedFilterByType.value as 'trans_date' | 'realization_date', + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, limit: 100, page: 1, }; @@ -244,11 +283,11 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { ? (response.data as unknown as CustomerPaymentReport[]) : null; }, [ - filterCustomer, - // filterSales, - filterStartDate, - filterEndDate, - filterByType, + appliedFilterCustomer, + // appliedFilterSales, + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterByType, ]); // ===== EXPORT HANDLERS ===== @@ -293,17 +332,19 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { data: allDataForExport, params: { customer_name: - filterCustomer.length > 0 - ? filterCustomer.map((c) => c.label).join(', ') + appliedFilterCustomer.length > 0 + ? appliedFilterCustomer.map((c) => c.label).join(', ') : undefined, // TODO: Uncomment when BE is ready // sales: - // filterSales.length > 0 - // ? filterSales.map((s) => s.label).join(', ') + // appliedFilterSales.length > 0 + // ? appliedFilterSales.map((s) => s.label).join(', ') // : undefined, - start_date: filterStartDate || undefined, - end_date: filterEndDate || undefined, - filter_by: filterByType.value as 'trans_date' | 'realization_date', + start_date: appliedFilterStartDate || undefined, + end_date: appliedFilterEndDate || undefined, + filter_by: appliedFilterByType.value as + | 'trans_date' + | 'realization_date', }, }); toast.success('PDF berhasil dibuat dan diunduh.'); From b4353cf834249f35d307cc75a0784a4c1e404b4c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 14:55:27 +0700 Subject: [PATCH 22/25] refactor(FE): Make filter type nullable and use applied filters --- .../report/finance/tab/CustomerPaymentTab.tsx | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 7160a273..e26abceb 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -56,8 +56,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // typeof salesOptions // >([]); const [appliedFilterByType, setAppliedFilterByType] = useState< - (typeof dataTypeOptions)[0] - >({ value: 'trans_date', label: 'Tanggal Jual/Bayar' }); + (typeof dataTypeOptions)[0] | null + >(null); const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); @@ -79,9 +79,9 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { [] ); - const [filterByType, setFilterByType] = useState<(typeof dataTypeOptions)[0]>( - dataTypeOptions[0] - ); + const [filterByType, setFilterByType] = useState< + (typeof dataTypeOptions)[0] | null + >(null); const { options: customerOptions, @@ -154,14 +154,14 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { const handleResetFilters = useCallback(() => { setIsSubmitted(false); setFilterCustomer([]); - setFilterByType(dataTypeOptions[0]); + setFilterByType(null); setFilterStartDate(''); setFilterEndDate(''); setAppliedFilterCustomer([]); - setAppliedFilterByType(dataTypeOptions[0]); + setAppliedFilterByType(null); setAppliedFilterStartDate(''); setAppliedFilterEndDate(''); - }, [dataTypeOptions]); + }, []); const handleApplyFilters = useCallback(() => { setAppliedFilterCustomer(filterCustomer); @@ -184,23 +184,33 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { let count = 0; // Date filter (start_date + end_date = 1 filter) - if (filterStartDate || filterEndDate) { + if (appliedFilterStartDate || appliedFilterEndDate) { count += 1; } // Customer filter - if (filterCustomer.length > 0) { + if (appliedFilterCustomer.length > 0) { + count += 1; + } + + // Filter by type filter (hanya dihitung jika ada nilai yang dipilih) + if (appliedFilterByType) { count += 1; } // TODO: Uncomment when BE is ready // // Sales filter - // if (filterSales.length > 0) { + // if (appliedFilterSales.length > 0) { // count += 1; // } return count; - }, [appliedFilterStartDate, appliedFilterEndDate, appliedFilterCustomer]); + }, [ + appliedFilterStartDate, + appliedFilterEndDate, + appliedFilterCustomer, + appliedFilterByType, + ]); const hasFilters = activeFiltersCount > 0; @@ -218,9 +228,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // appliedFilterSales.length > 0 // ? appliedFilterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: appliedFilterByType.value as + filter_by: appliedFilterByType?.value as | 'trans_date' - | 'realization_date', + | 'realization_date' + | undefined, start_date: appliedFilterStartDate || undefined, end_date: appliedFilterEndDate || undefined, page: currentPage, @@ -263,7 +274,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // appliedFilterSales.length > 0 // ? appliedFilterSales.map((v) => String(v.value)).join(',') // : undefined, - filter_by: appliedFilterByType.value as 'trans_date' | 'realization_date', + filter_by: appliedFilterByType?.value as + | 'trans_date' + | 'realization_date' + | undefined, start_date: appliedFilterStartDate || undefined, end_date: appliedFilterEndDate || undefined, limit: 100, @@ -342,9 +356,10 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { // : undefined, start_date: appliedFilterStartDate || undefined, end_date: appliedFilterEndDate || undefined, - filter_by: appliedFilterByType.value as + filter_by: appliedFilterByType?.value as | 'trans_date' - | 'realization_date', + | 'realization_date' + | undefined, }, }); toast.success('PDF berhasil dibuat dan diunduh.'); From 4fd4374e64a8722d20a15a4fb7119eaf968af7d5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 15:04:12 +0700 Subject: [PATCH 23/25] refactor(FE): Validate date range and show toast on error --- .../report/finance/tab/CustomerPaymentTab.tsx | 79 +++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index e26abceb..4e0e3f25 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -60,6 +60,8 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { >(null); const [appliedFilterStartDate, setAppliedFilterStartDate] = useState(''); const [appliedFilterEndDate, setAppliedFilterEndDate] = useState(''); + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); const [filterCustomer, setFilterCustomer] = useState( [] @@ -161,7 +163,12 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { setAppliedFilterByType(null); setAppliedFilterStartDate(''); setAppliedFilterEndDate(''); - }, []); + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, [dateErrorShown]); const handleApplyFilters = useCallback(() => { setAppliedFilterCustomer(filterCustomer); @@ -179,6 +186,67 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { filterEndDate, ]); + const handleStartDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterStartDate(value); + + if (value && filterEndDate) { + const startDate = new Date(value); + const endDateObj = new Date(filterEndDate); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }, + [filterEndDate, dateErrorShown] + ); + + const handleEndDateChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + setFilterEndDate(value); + + if (value && filterStartDate) { + const startDateObj = new Date(filterStartDate); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }, + [filterStartDate, dateErrorShown] + ); + // ===== ACTIVE FILTERS COUNT ===== const activeFiltersCount = useMemo(() => { let count = 0; @@ -872,9 +940,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { { - setFilterStartDate(e.target.value); - }} + onChange={handleStartDateChange} className={{ wrapper: 'w-full' }} isNestedModal /> @@ -883,9 +949,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { { - setFilterEndDate(e.target.value); - }} + onChange={handleEndDateChange} className={{ wrapper: 'w-full' }} isNestedModal /> @@ -951,6 +1015,7 @@ const CustomerPaymentTab = ({ tabId }: CustomerPaymentTabProps) => { From 70a9fa15eca83003abd1440784f651cb5707dbc4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 15:43:03 +0700 Subject: [PATCH 24/25] refactor(FE): Switch week input to SelectInputRadio --- .../delivery-order/DeliverOrderProduct.tsx | 22 ++++++++----------- .../sales-order/SalesOrderProductForm.tsx | 22 ++++++++----------- 2 files changed, 18 insertions(+), 26 deletions(-) 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 f5089c84..cdeded75 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -511,24 +511,20 @@ const DeliveryOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', Number(e.target.value)); - setCurrentInput(e.target.name); + onChange={(val) => { + formik.setFieldValue('week', val); + handleBlurField('week'); }} - onBlur={() => handleBlurField('week')} - isError={formik.touched.week && Boolean(formik.errors.week)} - errorMessage={formik.errors.week as string} - placeholder='Masukan Minggu' - decimalScale={0} + placeholder='Pilih Week' /> )} 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 ae1e4a44..d5327644 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -467,24 +467,20 @@ const SalesOrderProductForm = ({ {/* Konversi Satuan Week Pullet */} {formik.values.marketing_type?.value.toLowerCase() === 'ayam_pullet' && ( - { - formik.setFieldValue('week', Number(e.target.value)); - setCurrentInput(e.target.name); + onChange={(val) => { + formik.setFieldValue('week', val); + handleBlurField('week'); }} - onBlur={() => handleBlurField('week')} - isError={formik.touched.week && Boolean(formik.errors.week)} - errorMessage={formik.errors.week as string} - placeholder='Masukan Minggu' - decimalScale={0} + placeholder='Pilih Week' /> )} From d5eeadc9a7a3f4c5df8376714e23ca7cb1b6a57f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 5 Feb 2026 15:47:05 +0700 Subject: [PATCH 25/25] refactor(FE): Remove handleBlurField call on week change --- .../form/repeater/delivery-order/DeliverOrderProduct.tsx | 1 - .../form/repeater/sales-order/SalesOrderProductForm.tsx | 1 - 2 files changed, 2 deletions(-) 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 cdeded75..850d88d2 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -522,7 +522,6 @@ const DeliveryOrderProductForm = ({ } onChange={(val) => { formik.setFieldValue('week', val); - handleBlurField('week'); }} placeholder='Pilih Week' /> 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 d5327644..c718c40c 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -478,7 +478,6 @@ const SalesOrderProductForm = ({ } onChange={(val) => { formik.setFieldValue('week', val); - handleBlurField('week'); }} placeholder='Pilih Week' />