From 9237d4e731c062a5d402328b60266d46361ced66 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 17:10:42 +0700 Subject: [PATCH 01/13] fix(FE): implement lazy load select project flock --- .../project-flock/form/ProjectFlockForm.tsx | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 745a6b1e..0c252cb6 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -102,34 +102,47 @@ const ProjectFlockForm = ({ ); // Fetch Data - const { isLoadingOptions: isLoadingFlocks, options: optionsFlock } = - useSelect(FlockApi.basePath, 'id', 'name'); + const { + setInputValue: setInputValueFlock, + isLoadingOptions: isLoadingFlocks, + options: optionsFlock, + loadMore: loadMoreFlock, + } = useSelect(FlockApi.basePath, 'id', 'name', '', { + project_category: selectedCategory, + }); - const { options: optionsArea, isLoadingOptions: isLoadingAreas } = useSelect( - AreaApi.basePath, - 'id', - 'name' - ); + const { + setInputValue: setInputValueArea, + options: optionsArea, + isLoadingOptions: isLoadingAreas, + loadMore: loadMoreArea, + } = useSelect(AreaApi.basePath, 'id', 'name'); - const { options: optionsLocation, isLoadingOptions: isLoadingLocations } = - useSelect(LocationApi.basePath, 'id', 'name', '', { - area_id: - selectedArea != '' - ? selectedArea - : ((initialValues?.area?.id ?? '') as string), - }); + const { + options: optionsLocation, + isLoadingOptions: isLoadingLocations, + setInputValue: setInputValueLocation, + loadMore: loadMoreLocation, + } = useSelect(LocationApi.basePath, 'id', 'name', '', { + area_id: + selectedArea != '' + ? selectedArea + : ((initialValues?.area?.id ?? '') as string), + }); - const { options: optionsFcr, isLoadingOptions: isLoadingFcrs } = useSelect( - FcrApi.basePath, - 'id', - 'name' - ); + const { + options: optionsFcr, + isLoadingOptions: isLoadingFcrs, + setInputValue: setInputValueFcr, + loadMore: loadMoreFcr, + } = useSelect(FcrApi.basePath, 'id', 'name'); const { options: optionsProductionStandards, isLoadingOptions: isLoadingProductionStandards, + setInputValue: setInputValueProductionStandard, + loadMore: loadMoreProductionStandard, } = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', { - search: '', project_category: selectedCategory, }); @@ -153,6 +166,8 @@ const ProjectFlockForm = ({ options: optionsNonstock, rawData: nonstocks, isLoadingOptions: isLoadingNonstocks, + setInputValue: setInputValueNonstock, + loadMore: loadMoreNonstock, } = useSelect(NonstockApi.basePath, 'id', 'name'); useEffect(() => { @@ -722,6 +737,8 @@ const ProjectFlockForm = ({ formik.touched.area_id && Boolean(formik.errors.area_id) } errorMessage={formik.errors.area_id as string} + onInputChange={setInputValueArea} + onMenuScrollToBottom={loadMoreArea} isClearable isDisabled={formType != 'add'} /> @@ -740,6 +757,8 @@ const ProjectFlockForm = ({ formik.touched.location_id && Boolean(formik.errors.location_id) } + onInputChange={setInputValueLocation} + onMenuScrollToBottom={loadMoreLocation} errorMessage={formik.errors.location_id as string} isClearable isDisabled={formType != 'add' || disabledLocation} @@ -766,6 +785,8 @@ const ProjectFlockForm = ({ ); }} options={optionsFlock} + onInputChange={setInputValueFlock} + onMenuScrollToBottom={loadMoreFlock} isLoading={isLoadingFlocks} isError={ formik.touched.flock_name && Boolean(formik.errors.flock_name) @@ -781,6 +802,8 @@ const ProjectFlockForm = ({ onChange={(val) => { optionChangeHandler(val, 'fcr'); }} + onInputChange={setInputValueFcr} + onMenuScrollToBottom={loadMoreFcr} options={optionsFcr} isLoading={isLoadingFcrs} isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)} @@ -808,6 +831,8 @@ const ProjectFlockForm = ({ onChange={(val) => { optionChangeHandler(val, 'production_standard'); }} + onInputChange={setInputValueProductionStandard} + onMenuScrollToBottom={loadMoreProductionStandard} options={optionsProductionStandards} isLoading={isLoadingProductionStandards} isError={ @@ -892,6 +917,8 @@ const ProjectFlockForm = ({ isLoading={isLoadingNonstocks} placeholder='Pilih barang non stock' value={formik.values.project_budgets[index].nonstock} + onInputChange={setInputValueNonstock} + onMenuScrollToBottom={loadMoreNonstock} onChange={(val) => { const updatedBudgets = [ ...formik.values.project_budgets, From b52a414eb0543d1d62b8d1ff3e35ff32179e3188 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 19:11:38 +0700 Subject: [PATCH 02/13] fix(FE): adjust slicing ui debt supplier --- .../finance/export/DebtSupllierExportPDF.tsx | 149 +++++++++++++++--- .../report/finance/tab/DebtSupplierTab.tsx | 108 ++++++++++--- 2 files changed, 209 insertions(+), 48 deletions(-) diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index 083904a5..7782b212 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -18,6 +18,47 @@ Font.register({ src: 'helvetica', }); +// Status color mappings (same as in DebtSupplierTab) +const dueStatusColors: Record< + string, + { bg: string; text: string; border: string } +> = { + 'Sudah Jatuh Tempo': { bg: '#FEE2E2', text: '#991B1B', border: '#F87171' }, // error/red + 'Belum Jatuh Tempo': { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green + 'Mendekati Jatuh Tempo': { + bg: '#FEF3C7', + text: '#92400E', + border: '#FBBF24', + }, // warning/yellow +}; + +const paymentStatusColors: Record< + string, + { bg: string; text: string; border: string } +> = { + 'Belum Lunas': { bg: '#FEF3C7', text: '#92400E', border: '#FBBF24' }, // warning/yellow + Lunas: { bg: '#DBEAFE', text: '#1E40AF', border: '#60A5FA' }, // primary/blue + Pembayaran: { bg: '#D1FAE5', text: '#065F46', border: '#34D399' }, // success/green +}; + +/** + * Get badge style for PDF rendering + * @param statusText - The status text + * @param type - Type of status: 'due' or 'payment' + * @returns Style object with background and text colors + */ +const getPDFBadgeStyle = ( + statusText: string, + type: 'due' | 'payment' = 'payment' +) => { + const colors = + type === 'due' + ? dueStatusColors[statusText] + : paymentStatusColors[statusText]; + + return colors || { bg: '#F3F4F6', text: '#374151', border: '#D1D5DB' }; // neutral fallback +}; + const pdfStyles = StyleSheet.create({ page: { fontSize: 10, @@ -136,6 +177,16 @@ const pdfStyles = StyleSheet.create({ backgroundColor: '#F0F0F0', fontWeight: 'bold', }, + badge: { + paddingVertical: 2, + paddingHorizontal: 4, + borderRadius: 12, + fontSize: 5, + fontWeight: 'bold', + borderWidth: 1, + textAlign: 'center', + whiteSpace: 'nowrap', + }, }); interface DebtSupplierExportPDFParams { @@ -193,7 +244,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Jatuh Tempo - + Status Jatuh Tempo @@ -205,7 +256,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Sisa Saldo Hutang (Rp) - + Status @@ -216,40 +267,40 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { {/* Initial Balance Row */} - + {/* NO */} - + {/* No. PR */} - + {/* No. PO */} - + {/* Tgl Terima/Bayar */} - + {/* Tgl PO */} - + {/* Aging */} - + {/* Area */} - + {/* Gudang */} - + {/* Jatuh Tempo */} - - + + {/* Status Jatuh Tempo */} - + {/* Nominal Pembelian (Rp) */} - + {/* Pembayaran (Rp) */} { ]} > + {' '} + {/* Sisa Saldo Hutang (Rp) */} {formatCurrency(supplierReport.initial_balance || 0)} - - + + {/* Status */} - + {/* No. Perjalanan */} @@ -328,8 +381,32 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { : '-'} - - {item.due_status || '-'} + + {item.due_status && item.due_status !== '-' ? ( + + + {item.due_status} + + + ) : ( + - + )} { > {formatCurrency(item.balance)} - - {item.status || '-'} + + {item.status && item.status !== '-' ? ( + + + {item.status} + + + ) : ( + - + )} {item.travel_number || '-'} @@ -400,7 +501,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { - + { > {formatCurrency(supplierReport.total.debt_price)} - + diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 2214ecd6..1f5d79b9 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -9,9 +9,9 @@ import SelectInput, { import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Modal, { useModal } from '@/components/Modal'; -import Table from '@/components/Table'; +import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; import { isResponseSuccess } from '@/lib/api-helper'; -import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { SupplierApi } from '@/services/api/master-data'; import { DebtRow, @@ -31,8 +31,46 @@ import { DebtSupplierFilterSchema, DebtSupplierFilterType, } from '@/components/pages/report/finance/filter/DebtSupplierFilter'; -import { getFilledFormikValuesCount } from '@/lib/formik-helper'; import ButtonFilter from '@/components/helper/ButtonFilter'; +import Badge from '@/components/Badge'; +import { Color } from '@/types/theme'; + +const dueStatus: Record = { + 'Sudah Jatuh Tempo': 'error', + 'Belum Jatuh Tempo': 'success', + 'Mendekati Jatuh Tempo': 'warning', +}; + +const paymentStatus: Record = { + 'Belum Lunas': 'warning', + Lunas: 'primary', + Pembayaran: 'success', +}; + +const getPillBadge = ( + statusText: string, + type: 'due' | 'payment' = 'payment' +) => { + // Get color based on type + const color = + type === 'due' + ? dueStatus[statusText] || 'neutral' + : paymentStatus[statusText] || 'neutral'; + + return ( + + {statusText} + + ); +}; const DebtSupplierTab = () => { // ===== STATE MANAGEMENT ===== @@ -224,6 +262,7 @@ const DebtSupplierTab = () => { header: 'No', enableSorting: false, cell: (props) => props.row.index, + footer: () => 'Total', }, { id: 'pr_number', @@ -328,7 +367,7 @@ const DebtSupplierTab = () => { enableSorting: false, cell: (props) => { const value = props.row.original.due_status; - return value || '-'; + return value ? (value != '-' ? getPillBadge(value, 'due') : '-') : '-'; }, }, { @@ -404,7 +443,11 @@ const DebtSupplierTab = () => { enableSorting: false, cell: (props) => { const value = props.row.original.status; - return value || '-'; + return value + ? value != '-' + ? getPillBadge(value, 'payment') + : '-' + : '-'; }, }, { @@ -472,9 +515,15 @@ const DebtSupplierTab = () => { { renderFooter={supplierReport.rows.length > 0} className={{ containerClassName: 'w-full', - tableWrapperClassName: 'overflow-x-auto mt-4', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableWrapperClassName: 'overflow-x-auto', + headerColumnClassName: cn( + TABLE_DEFAULT_STYLING.headerColumnClassName, + 'whitespace-nowrap' + ), + bodyColumnClassName: cn( + TABLE_DEFAULT_STYLING.bodyColumnClassName, + 'whitespace-nowrap' + ), + footerRowClassName: cn( + TABLE_DEFAULT_STYLING.footerRowClassName, + 'bg-white' + ), + footerColumnClassName: cn( + TABLE_DEFAULT_STYLING.footerColumnClassName, + 'whitespace-nowrap p-3' + ), paginationClassName: 'hidden', }} renderCustomRow={(row) => { if (row.index == 0) { return ( - From 7a6bee57c2aeedde1b0864f4ab3ca437735cb4fb Mon Sep 17 00:00:00 2001 From: randy-ar Date: Wed, 14 Jan 2026 19:20:16 +0700 Subject: [PATCH 03/13] fix(FE): fix report excel debt supplier change debt_price to balance --- .../pages/report/finance/export/DebtSupplierExportXLSX.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx index 3ba96a22..dfd5ee95 100644 --- a/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx +++ b/src/components/pages/report/finance/export/DebtSupplierExportXLSX.tsx @@ -64,7 +64,7 @@ export const generateDebtSupplierExcel = ( 'Status Jatuh Tempo': item.due_status || '', 'Nominal Pembelian (Rp)': item.total_price || 0, 'Pembayaran (Rp)': item.payment_price || 0, - 'Sisa Saldo Hutang (Rp)': item.debt_price || 0, + 'Sisa Saldo Hutang (Rp)': item.balance || 0, Status: item.status || '', 'Nomor Perjalanan': item.travel_number || '', })), From 5e7f55000a6398a069a4bbe25174a1272e1818c0 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 00:32:55 +0700 Subject: [PATCH 04/13] fix(FE): add sales person, refactor calculate total weight and price, add uom information and implement lazy load select --- .../marketing/form/MarketingForm.schema.ts | 13 +- .../pages/marketing/form/MarketingForm.tsx | 76 +++++++-- .../delivery-order/DeliverOrderProduct.tsx | 134 +++++++++++---- .../sales-order/SalesOrderProductForm.tsx | 161 ++++++++++++------ .../recording/form/RecordingForm.tsx | 2 +- 5 files changed, 286 insertions(+), 100 deletions(-) diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index d81cdb9c..b09129c3 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -11,6 +11,13 @@ import { type MarketingSchemaType = { customer_id: number | undefined; sales_person_id: number | undefined; + sales_person: + | { + value: number; + label: string; + } + | undefined + | null; customer: | { value: number; @@ -33,7 +40,11 @@ type DeliveryOrderSchemaType = { export const SalesOrderSchema: Yup.ObjectSchema = Yup.object({ customer_id: Yup.number().required('Customer wajib diisi!'), - sales_person_id: Yup.number().required('Sales Person wajib diisi!'), + sales_person_id: Yup.number().required('Sales wajib diisi!'), + sales_person: Yup.object({ + value: Yup.number().required(), + label: Yup.string().required(), + }).nullable(), customer: Yup.object({ value: Yup.number().required(), label: Yup.string().required(), diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx index 2fbca835..be4367cb 100644 --- a/src/components/pages/marketing/form/MarketingForm.tsx +++ b/src/components/pages/marketing/form/MarketingForm.tsx @@ -50,6 +50,8 @@ import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/for 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 MemoizedSalesOrderProductTable = memo(SalesOrderProductTable); const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm); @@ -244,7 +246,15 @@ const MarketingForm = ({ 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< @@ -255,6 +265,12 @@ const MarketingForm = ({ 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, @@ -443,6 +459,13 @@ const MarketingForm = ({ }, [] ); + 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]); @@ -580,6 +603,7 @@ const MarketingForm = ({ className={{ wrapper: 'bg-white w-full', }} + variant='bordered' >
- -
+
+ + +
+
Total Penjualan {formatCurrency(grandTotal)}{' '} 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 25a20982..da3b2fee 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -18,6 +18,11 @@ import * as Yup from 'yup'; import { isResponseSuccess } from '@/lib/api-helper'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import useSWR from 'swr'; +import { ProductApi } from '@/services/api/master-data'; + +const roundWeight = (value: number) => Number(value.toFixed(2)); +const roundPrice = (value: number) => Math.round(value); const DeliveryOrderProductForm = ({ formState, @@ -43,6 +48,17 @@ const DeliveryOrderProductForm = ({ ); const [currentInput, setCurrentInput] = useState(''); + // ============ Fetch Data ============ + const { data: productData } = useSWR( + selectedProduct?.value + ? ProductApi.basePath + '/' + selectedProduct?.value + : null, + () => + selectedProduct?.value + ? ProductApi.getSingle(Number(selectedProduct?.value)) + : undefined + ); + const salesOrder = salesOrders.find( (item) => item.id === initialValues?.marketing_product_id ); @@ -113,22 +129,60 @@ const DeliveryOrderProductForm = ({ const handleBlurField = (field: string) => { setCurrentInput(field); - const { qty, unit_price, total_price, avg_weight, total_weight } = - formik.values; - if (field === 'unit_price' || field === 'total_price' || field === 'qty') { - if (qty && unit_price && (field === 'unit_price' || field === 'qty')) { - formik.setFieldValue('total_price', Number(qty) * Number(unit_price)); - } else if (qty && total_price && field === 'total_price') { - formik.setFieldValue('unit_price', Number(total_price) / Number(qty)); + 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) { + formik.setFieldValue('total_weight', roundWeight(qty * avgWeight)); + } + + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + break; } - } - if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') { - if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) { - formik.setFieldValue('total_weight', Number(qty) * Number(avg_weight)); - } else if (qty && total_weight && field === 'total_weight') { - formik.setFieldValue('avg_weight', Number(total_weight) / Number(qty)); + case 'avg_weight': { + if (avgWeight > 0) { + const tw = roundWeight(qty * avgWeight); + formik.setFieldValue('total_weight', tw); + + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + } + break; + } + + case 'unit_price': { + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + break; + } + + // ===== TOTAL EDITABLE ===== + case 'total_weight': { + if (totalWeight > 0) { + formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty)); + } + break; + } + + case 'total_price': { + if (totalPrice > 0) { + formik.setFieldValue('unit_price', roundPrice(totalPrice / qty)); + } + break; } } }; @@ -183,7 +237,7 @@ const DeliveryOrderProductForm = ({
)} -
+
- +
+
+
+ + {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 + )?.qty + + ' ' + + (isResponseSuccess(productData) + ? productData?.data?.uom.name + : '') : '' } /> -
-
-
- { - 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('avg_weight')} + isError={Boolean(formik.errors.avg_weight)} + errorMessage={formik.errors.avg_weight} + placeholder='Masukan Bobot Rata-rata' + /> Number(value.toFixed(2)); +const roundPrice = (value: number) => Math.round(value); const SalesOrderProductForm = ({ initialValues, @@ -39,6 +43,19 @@ const SalesOrderProductForm = ({ }) => { const [formErrorMessage, setFormErrorMessage] = useState(''); const [currentInput, setCurrentInput] = useState(''); + const [selectedProductWarehouse, setSelectedProductWarehouse] = + useState(null); + + // ============ Fetch Data ============ + const { data: productData } = useSWR( + selectedProductWarehouse?.product_id + ? ProductApi.basePath + '/' + selectedProductWarehouse?.product_id + : null, + () => + selectedProductWarehouse?.product_id + ? ProductApi.getSingle(selectedProductWarehouse?.product_id) + : undefined + ); // ============ Formik ============ const formik = useFormik({ @@ -69,17 +86,21 @@ const SalesOrderProductForm = ({ const { options: kandangSourceOptions, isLoadingOptions: isLoadingKandangSourceOptions, + setInputValue: setKandangInputValue, + loadMore: loadMoreKandang, } = useSelect(WarehouseApi.basePath, 'id', 'name'); const { options: warehouseSourceOptions, rawData: warehouseSourceRawData, isLoadingOptions: isLoadingWarehouseSourceOptions, + setInputValue: setWarehouseInputValue, + loadMore: loadMoreWarehouse, } = useSelect( ProductWarehouseApi.basePath, 'id', 'product.name', - 'search', + '', { warehouse_id: formik.values.kandang_id?.toString() ?? '', } @@ -112,6 +133,7 @@ const SalesOrderProductForm = ({ const productWarehouse = warehouseSourceRawData?.data.find( (item: ProductWarehouse) => item.id === newId ); + setSelectedProductWarehouse(productWarehouse || null); formik.setFieldValue('qty', productWarehouse?.quantity); handleBlurField('qty'); } else { @@ -139,34 +161,60 @@ const SalesOrderProductForm = ({ const handleBlurField = (field: string) => { setCurrentInput(field); - const { qty, unit_price, total_price, avg_weight, total_weight } = - formik.values; - if (field === 'unit_price' || field === 'total_price' || field === 'qty') { - if (qty && unit_price && (field === 'unit_price' || field === 'qty')) { - formik.setFieldValue( - 'total_price', - (qty as number) * (unit_price as number) - ); - } else if (qty && total_price && field === 'total_price') { - formik.setFieldValue( - 'unit_price', - (total_price as number) / (qty as number) - ); + 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) { + formik.setFieldValue('total_weight', roundWeight(qty * avgWeight)); + } + + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + break; } - } - if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') { - if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) { - formik.setFieldValue( - 'total_weight', - (qty as number) * (avg_weight as number) - ); - } else if (qty && total_weight && field === 'total_weight') { - formik.setFieldValue( - 'avg_weight', - (total_weight as number) / (qty as number) - ); + case 'avg_weight': { + if (avgWeight > 0) { + const tw = roundWeight(qty * avgWeight); + formik.setFieldValue('total_weight', tw); + + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + } + break; + } + + case 'unit_price': { + if (unitPrice > 0) { + formik.setFieldValue('total_price', roundPrice(qty * unitPrice)); + } + break; + } + + // ===== TOTAL EDITABLE ===== + case 'total_weight': { + if (totalWeight > 0) { + formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty)); + } + break; + } + + case 'total_price': { + if (totalPrice > 0) { + formik.setFieldValue('unit_price', roundPrice(totalPrice / qty)); + } + break; } } }; @@ -188,7 +236,7 @@ const SalesOrderProductForm = ({
)} -
+
+
+
+
+ + {isResponseSuccess(productData) + ? productData?.data?.uom.name + : ''} + +
+ } bottomLabel={ isResponseSuccess(warehouseSourceRawData) && formik.values.product_warehouse_id @@ -264,32 +328,13 @@ const SalesOrderProductForm = ({ (item) => item.id === formik.values.product_warehouse_id )?.quantity ?? 0 )} ${ - warehouseSourceRawData?.data?.find( - (item) => item.id === formik.values.product_warehouse_id - )?.product?.uom?.name ?? '' + isResponseSuccess(productData) + ? productData?.data?.uom.name + : '' }` : '' } /> -
-
-
- { - 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('avg_weight')} + isError={ + formik.touched.avg_weight && Boolean(formik.errors.avg_weight) + } + errorMessage={formik.errors.avg_weight} + placeholder='Masukan Bobot Rata-rata' + /> { todayRecordings.forEach((recording) => { const recordingDate = recording.record_datetime?.split('T')[0]; if (recordingDate === today) { - recordedIds.add(recording.project_flock.project_flock_kandang_id); + recordedIds.add(recording.project_flock?.project_flock_kandang_id); } }); From 76e15d13ad40eec16236d185417ddbd830d67c51 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 00:52:29 +0700 Subject: [PATCH 05/13] fix(FE): adding filter information and supplier category on export pdf --- .../finance/export/DebtSupllierExportPDF.tsx | 61 +++++++++++++++++++ .../report/finance/tab/DebtSupplierTab.tsx | 12 +++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx index 7782b212..869430b0 100644 --- a/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx +++ b/src/components/pages/report/finance/export/DebtSupllierExportPDF.tsx @@ -187,10 +187,30 @@ const pdfStyles = StyleSheet.create({ textAlign: 'center', whiteSpace: 'nowrap', }, + parameterBadge: { + backgroundColor: '#F5F5F5', + color: '#333333', + padding: 4, + borderRadius: 4, + fontSize: 8, + marginRight: 8, + marginBottom: 4, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, }); interface DebtSupplierExportPDFParams { data: DebtSupplier[]; + params?: { + supplier_name?: string; + start_date?: string; + end_date?: string; + filter_by?: string; + }; } const createPDFDocument = (params: DebtSupplierExportPDFParams) => { @@ -208,9 +228,50 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => { Laporan > Rekapitulasi Hutang ke Supplier + + + + Periode:{' '} + {params.params?.start_date + ? formatDate(params.params.start_date, 'DD MMM YYYY') + : '-'}{' '} + s.d{' '} + {params.params?.end_date + ? formatDate(params.params.end_date, 'DD MMM YYYY') + : '-'} + + + {params.params?.filter_by && ( + + + Filter Tanggal:{' '} + {params.params.filter_by === 'po_date' + ? 'Tanggal PO' + : params.params.filter_by === 'received_date' + ? 'Tanggal Terima' + : params.params.filter_by === 'due_date' + ? 'Tanggal Jatuh Tempo' + : params.params.filter_by} + + + )} + + + Supplier: {params.params?.supplier_name || 'Semua Supplier'} + + + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + {supplierReport.supplier.name} + + {supplierReport.supplier.category} + {/* Table */} diff --git a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx index 1f5d79b9..0d53488c 100644 --- a/src/components/pages/report/finance/tab/DebtSupplierTab.tsx +++ b/src/components/pages/report/finance/tab/DebtSupplierTab.tsx @@ -247,7 +247,17 @@ const DebtSupplierTab = () => { return; } - await generateDebtSupplierPDF({ data: allDataForExport }); + await generateDebtSupplierPDF({ + data: allDataForExport, + params: { + supplier_name: formik.values.supplierIds + ?.map((v) => v.label) + .join(', '), + filter_by: formik.values.filterBy?.label, + start_date: formik.values.startDate || undefined, + end_date: formik.values.endDate || undefined, + }, + }); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); From 781a5ca0d975b0d5969403fddfe7261247f5d971 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:04 +0700 Subject: [PATCH 06/13] chore: use real permission for daily checklist menu --- src/config/constant.ts | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/config/constant.ts b/src/config/constant.ts index d3832613..b3621c8f 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -10,61 +10,65 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Daily Checklist', link: '/daily-checklist', icon: 'heroicons-outline:clipboard-check', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: [ + 'lti.daily_checklist.dashboard.list', + 'lti.daily_checklist.create', + 'lti.daily_checklist.list', + 'lti.daily_checklist.detail', + 'lti.daily_checklist.reports', + 'lti.daily_checklist.master_data.employee', + 'lti.daily_checklist.master_data.activity', + 'lti.daily_checklist.master_data.configuration', + ], submenu: [ { text: 'Dashboard', link: '/daily-checklist/dashboard', icon: 'lucide:layout-dashboard', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.dashboard.list'], }, { text: 'Daily Checklist', link: '/daily-checklist/daily-checklist', icon: 'lucide:clipboard-check', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.create'], }, { text: 'Daftar Daily Checklist', link: '/daily-checklist/list-daily-checklist', icon: 'lucide:circle-check', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.list'], }, { text: 'Laporan', link: '/daily-checklist/reports', icon: 'lucide:file-text', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.reports'], }, { text: 'Master Data', link: '/daily-checklist/master-data', icon: 'lucide:database', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: [ + 'lti.daily_checklist.master_data.employee', + 'lti.daily_checklist.master_data.activity', + 'lti.daily_checklist.master_data.configuration', + ], submenu: [ { text: 'Employee (ABK)', link: '/daily-checklist/master-data/employee', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.master_data.employee'], }, { text: 'Aktivitas', link: '/daily-checklist/master-data/activity', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.master_data.activity'], }, { text: 'Konfigurasi', link: '/daily-checklist/master-data/configuration', - // TODO: add permission - // permission: ['lti.daily_checklist.list'], + permission: ['lti.daily_checklist.master_data.configuration'], }, ], }, From 8f55ced55a3c89029a77493f128999ec9edba26f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:17 +0700 Subject: [PATCH 07/13] feat: add export to pdf functionality --- .../ProductionResultContent.tsx | 102 +++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/production-result/ProductionResultContent.tsx b/src/components/pages/report/production-result/ProductionResultContent.tsx index 7820ff53..28d334e8 100644 --- a/src/components/pages/report/production-result/ProductionResultContent.tsx +++ b/src/components/pages/report/production-result/ProductionResultContent.tsx @@ -21,10 +21,18 @@ import { ProjectFlockApi, ProjectFlockKandangApi, } from '@/services/api/production'; -import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -import { isResponseError } from '@/lib/api-helper'; +import { + BaseProjectFlockKandang, + ProjectFlockKandang, +} from '@/types/api/production/project-flock-kandang'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import Pagination from '@/components/Pagination'; import { ProductionResultReportApi } from '@/services/api/report/production-result'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { httpClient } from '@/services/http/client'; +import { ProductionResult } from '@/types/api/report/production-result'; +import ProductionResultReportPDF from './ProductionResultReportPDF'; +import { pdf } from '@react-pdf/renderer'; const ProductionResultContent = () => { const [projectFlockKandangs, setProjectFlockKandangs] = useState< @@ -49,6 +57,8 @@ const ProductionResultContent = () => { const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = useState(false); + const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false); + const [selectedArea, setSelectedArea] = useState(null); const [selectedLocation, setSelectedLocation] = useState( null @@ -158,6 +168,87 @@ const ProductionResultContent = () => { setIsLoadingExportingToExcel(false); }; + const exportToPdfHandler = async () => { + setIsLoadingExportingToPdf(true); + + try { + let projectFlockKandangsData: BaseProjectFlockKandang[] = []; + + if (selectedProjectFlockKandang) { + const projectFlockKandangResponse = + await ProjectFlockKandangApi.getSingle( + selectedProjectFlockKandang?.value as number + ); + + projectFlockKandangsData = isResponseSuccess( + projectFlockKandangResponse + ) + ? [projectFlockKandangResponse.data] + : []; + } else { + const projectFlockKandangsResponse = + await ProjectFlockKandangApi.getAll({ + area_id: selectedArea?.value, + project_flock_id: selectedProjectFlock?.value, + }); + + projectFlockKandangsData = isResponseSuccess( + projectFlockKandangsResponse + ) + ? projectFlockKandangsResponse.data + : []; + } + + const mappedProductionResults: { + projectFlockKandang: BaseProjectFlockKandang; + productionResult: ProductionResult[] | null; + }[] = await Promise.all( + projectFlockKandangsData.map(async (projectFlockKandang) => { + const getProductionResultPath = `${ProductionResultReportApi.basePath}/${projectFlockKandang.id}?page=1&limit=100`; + const getProductionResultRes = await httpClient< + BaseApiResponse + >(getProductionResultPath); + + return { + projectFlockKandang, + productionResult: isResponseSuccess(getProductionResultRes) + ? getProductionResultRes.data + : null, + }; + }) + ); + + if (mappedProductionResults.length === 0) { + toast.error('Tidak ada data untuk diexport.'); + setIsLoadingExportingToPdf(false); + return; + } + + const openPdf = async () => { + const productionResultPdfBlob = await pdf( + + ).toBlob(); + + const productionResultReportPdfUrl = URL.createObjectURL( + productionResultPdfBlob + ); + window.open(productionResultReportPdfUrl, '_blank'); + }; + + await openPdf(); + } catch (error) { + console.error(error); + toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.'); + } + // await ProductionResultReportApi.exportProductionResultToPdf( + // projectFlockKandangs + // ); + + setIsLoadingExportingToPdf(false); + }; + const searchHandler = async () => { setProjectFlockKandangs(null); setIsLoadingSearch(true); @@ -355,6 +446,13 @@ const ProductionResultContent = () => { onClick={exportToExcelHandler} className='text-nowrap' /> +
From e15b7e11d3bd7075d76dd2ffe418da6a3b48133e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:33 +0700 Subject: [PATCH 08/13] feat: create ProductionResultReportPDF component --- .../ProductionResultReportPDF.tsx | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 src/components/pages/report/production-result/ProductionResultReportPDF.tsx diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx new file mode 100644 index 00000000..9bc27c4b --- /dev/null +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -0,0 +1,388 @@ +'use client'; + +import React from 'react'; +import { + Document, + Page, + StyleSheet, + Text, + View, + Image, +} from '@react-pdf/renderer'; + +import { formatDate, formatNumber } from '@/lib/helper'; +import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; +import { ProductionResult } from '@/types/api/report/production-result'; + +type MappedProductionResultsItem = { + projectFlockKandang: BaseProjectFlockKandang; + productionResult: ProductionResult[] | null; +}; + +interface ProductionResultReportPDFProps { + mappedProductionResults?: MappedProductionResultsItem[]; +} + +const styles = StyleSheet.create({ + page: { + paddingTop: 24, + paddingBottom: 52, + paddingHorizontal: 16, + }, + + companyInfoHeader: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 8, + }, + companyLogo: { + width: 64, + height: 'auto', + }, + companyInfoHeaderDate: { + paddingTop: 8, + fontSize: 10, + }, + companyName: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 4, + }, + companyAddress: { + fontSize: 8, + maxWidth: 420, + marginBottom: 10, + }, + doubleDivider: { + width: '100%', + height: 6, + borderTopWidth: 2, + borderTopColor: '#000', + borderBottomWidth: 2, + borderBottomColor: '#000', + }, + + title: { + marginTop: 14, + fontSize: 14, + lineHeight: '150%', + textAlign: 'center', + fontFamily: 'Times-Roman', + fontWeight: 'bold', + }, + + footer: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + position: 'absolute', + fontSize: 8, + bottom: 22, + left: 0, + right: 0, + textAlign: 'center', + color: 'grey', + }, + + section: { + marginTop: 12, + borderWidth: 1, + borderColor: '#000', + padding: 8, + }, + + sectionHeader: { + marginBottom: 6, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'baseline', + }, + sectionTitle: { + fontSize: 10, + fontWeight: 'bold', + }, + sectionSubtitle: { + fontSize: 8, + color: '#444', + }, + + // Simple grid table (label/value pairs) + grid: { + width: '100%', + borderWidth: 1, + borderColor: '#000', + }, + gridRow: { + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: '#000', + }, + gridRowLast: { + borderBottomWidth: 0, + }, + gridCellLabel: { + width: '40%', + paddingVertical: 3, + paddingHorizontal: 6, + fontSize: 8, + borderRightWidth: 1, + borderRightColor: '#000', + fontWeight: 'bold', + }, + gridCellValue: { + width: '60%', + paddingVertical: 3, + paddingHorizontal: 6, + fontSize: 8, + textAlign: 'right', + }, + + // Subsection headings + groupTitle: { + marginTop: 8, + marginBottom: 4, + fontSize: 9, + fontWeight: 'bold', + }, + + emptyText: { + fontSize: 8, + color: '#666', + fontStyle: 'italic', + }, +}); + +function safeNum(v: unknown): number { + const n = typeof v === 'number' ? v : Number(v); + return Number.isFinite(n) ? n : 0; +} + +function valueText(v: unknown) { + if (v === null || v === undefined) return '-'; + if (typeof v === 'number') return formatNumber(v); + return String(v); +} + +/** + * Render label/value table for one ProductionResult. + * Uses a compact grid to keep page readable. + */ +function ProductionResultGrid({ pr }: { pr: ProductionResult }) { + const rows: Array<[string, string]> = [ + ['WOA', valueText(pr.woa)], + + // BW + ['BW', valueText(pr.bw)], + ['Std BW', valueText(pr.std_bw)], + ['Uniformity', valueText(pr.uniformity)], + ['Std Uniformity', valueText(pr.std_uniformity)], + + // Dep + ['Dep Kum', valueText(pr.dep_kum)], + ['Dep Std', valueText(pr.dep_std)], + + // Butiran + ['Butiran Utuh', valueText(pr.butiran_utuh)], + ['Butiran Putih', valueText(pr.butiran_putih)], + ['Butiran Retak', valueText(pr.butiran_retak)], + ['Butiran Pecah', valueText(pr.butiran_pecah)], + ['Butiran Jumlah', valueText(pr.butiran_jumlah)], + ['Total Butir', valueText(pr.total_butir)], + + // Kg + ['Kg Utuh', valueText(pr.kg_utuh)], + ['Kg Putih', valueText(pr.kg_putih)], + ['Kg Retak', valueText(pr.kg_retak)], + ['Kg Pecah', valueText(pr.kg_pecah)], + ['Kg Jumlah', valueText(pr.kg_jumlah)], + ['Total Kg', valueText(pr.total_kg)], + + // % + ['% Utuh', valueText(pr.persen_utuh)], + ['% Putih', valueText(pr.persen_putih)], + ['% Retak', valueText(pr.persen_retak)], + ['% Pecah', valueText(pr.persen_pecah)], + + // Produksi + ['HD', valueText(pr.hd)], + ['HD Std', valueText(pr.hd_std)], + ['FI', valueText(pr.fi)], + ['FI Std', valueText(pr.fi_std)], + ['EM', valueText(pr.em)], + ['EM Std', valueText(pr.em_std)], + ['EW', valueText(pr.ew)], + ['EW Std', valueText(pr.ew_std)], + ['FCR', valueText(pr.fcr)], + ['FCR Std', valueText(pr.fcr_std)], + ['HH', valueText(pr.hh)], + ['HH Std', valueText(pr.hh_std)], + ]; + + return ( + + {rows.map(([label, value], idx) => { + const isLast = idx === rows.length - 1; + return ( + + {label} + {value} + + ); + })} + + ); +} + +/** + * If there are multiple ProductionResult entries for a kandang, + * we show them sequentially with a small header per result. + * + * You can later change this to render only the latest WOA, or group by week. + */ +function ProductionResultList({ + productionResults, +}: { + productionResults: ProductionResult[]; +}) { + return ( + + {productionResults.map((pr, idx) => { + const kandangName = + pr.project_flock?.kandang?.name || + pr.project_flock?.kandang?.id?.toString() || + ''; + + // Optional: show a compact subheader + const headerLeft = `Data #${idx + 1}`; + const headerRight = + kandangName && pr.woa !== undefined + ? `${kandangName} • WOA ${safeNum(pr.woa)}` + : pr.woa !== undefined + ? `WOA ${safeNum(pr.woa)}` + : ''; + + return ( + + + {headerLeft} + {headerRight} + + + + + ); + })} + + ); +} + +/** + * ✅ Main PDF Component + */ +const ProductionResultReportPDF = ({ + mappedProductionResults = [], +}: ProductionResultReportPDFProps) => { + return ( + + + {/* Header */} + + + + + {formatDate(Date.now(), 'DD MMMM YYYY')} + + + + + PT LUMBUNG TELUR INDONESIA + + SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. + Cipedes, Kec. Sukajadi, Kota Bandung 40162 + + + + + + + Laporan Production Result + + {/* Sections per ProjectFlockKandang */} + {mappedProductionResults.length === 0 ? ( + + Tidak ada data. + + ) : ( + mappedProductionResults.map((item, idx) => { + const pfk = item.projectFlockKandang; + + // Try to display meaningful identifiers. + // Adjust these fields based on your real BaseProjectFlockKandang structure. + const kandangName = + pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`; + + const projectName = pfk?.project_flock?.name ?? ''; + + const locationName = pfk?.project_flock?.location?.name ?? ''; + + const areaName = pfk?.project_flock?.area?.name ?? ''; + + return ( + 0} // each kandang starts on a new page for clarity + > + + + {projectName + ? `${projectName} • ${kandangName}` + : kandangName} + + + {[areaName, locationName].filter(Boolean).join(' • ')} + + + + {item.productionResult && item.productionResult.length > 0 ? ( + + ) : ( + + Tidak ada production result untuk kandang ini. + + )} + + ); + }) + )} + + {/* Footer */} + + + `${pageNumber} / ${totalPages}` + } + fixed + /> + + + + ); +}; + +export default ProductionResultReportPDF; From bd64694c7324619a6394fe801edf3f1e1b436e29 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:56:30 +0700 Subject: [PATCH 09/13] feat: implement closing sapronak per kandang --- .../pages/closing/ClosingIncomingSapronaksTable.tsx | 6 +++++- .../pages/closing/ClosingOutgoingSapronaksTable.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx index 53e45710..eda7e756 100644 --- a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx +++ b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx @@ -1,6 +1,7 @@ 'use client'; import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; @@ -23,6 +24,9 @@ interface ClosingIncomingSapronaksTableProps { const ClosingIncomingSapronaksTable = ({ projectFlockId, }: ClosingIncomingSapronaksTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { state: tableFilterState, updateFilter, @@ -43,7 +47,7 @@ const ClosingIncomingSapronaksTable = ({ const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming`, + `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`, ClosingApi.getAllIncomingSapronakFetcher, { keepPreviousData: true, diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx index 5662cff1..ac918561 100644 --- a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx @@ -1,6 +1,7 @@ 'use client'; import { ChangeEventHandler, useEffect, useState } from 'react'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ColumnDef, SortingState } from '@tanstack/react-table'; @@ -23,6 +24,9 @@ interface ClosingOutgoingSapronaksTableProps { const ClosingOutgoingSapronaksTable = ({ projectFlockId, }: ClosingOutgoingSapronaksTableProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { state: tableFilterState, updateFilter, @@ -43,7 +47,7 @@ const ClosingOutgoingSapronaksTable = ({ const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing`, + `${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`, ClosingApi.getAllOutgoingSapronakFetcher, { keepPreviousData: true, From fce2cfee736efea0828bcca11e62fbf85e6a49a5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:56:47 +0700 Subject: [PATCH 10/13] feat: implement closing production data per kandang --- .../pages/closing/ClosingProductionDataTabContent.tsx | 8 ++++++-- src/services/api/closing.ts | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx index 0f15d5b9..9295d283 100644 --- a/src/components/pages/closing/ClosingProductionDataTabContent.tsx +++ b/src/components/pages/closing/ClosingProductionDataTabContent.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -12,9 +13,12 @@ interface ClosingProductionDataTabContentProps { const ClosingProductionDataTabContent = ({ projectFlockId, }: ClosingProductionDataTabContentProps) => { + const searchParams = useSearchParams(); + const kandangId = searchParams.get('kandangId'); + const { data: productionData, isLoading } = useSWR( - `${ClosingApi.basePath}/${projectFlockId}/production-data`, - () => ClosingApi.getProductionData(projectFlockId) + `${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`, + () => ClosingApi.getProductionData(projectFlockId, Number(kandangId)) ); if (isLoading) { diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index b2ba2b8f..323e09e8 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -91,10 +91,11 @@ export class ClosingApiService extends BaseApiService { } async getProductionData( - id: number + id: number, + kandangId?: number ): Promise | undefined> { try { - const getProductionDataPath = `${this.basePath}/${id}/production-data`; + const getProductionDataPath = `${this.basePath}/${id}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`; const getProductionDataRes = await httpClient< BaseApiResponse >(getProductionDataPath); From 438082c94cfb1d51042cfb7bdbddac5a9e3b2f67 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 15 Jan 2026 16:05:05 +0700 Subject: [PATCH 11/13] fix(FE): fixing error message on submit and fixing ui --- .../form/ProductionStandardForm.schema.ts | 34 ++--- .../form/ProductionStandardForm.tsx | 131 ++++++++++++------ src/services/hooks/useFormikErrorList.ts | 3 + 3 files changed, 104 insertions(+), 64 deletions(-) diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts index 13183e71..eb59a9c0 100644 --- a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts +++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts @@ -2,34 +2,30 @@ import * as Yup from 'yup'; // Schema for LAYING category (production_standard_details is required) const LayingRepeaterFormSchema = Yup.object({ - week: Yup.number().required('Minggu wajib diisi!'), + week: Yup.number().required('Wajib diisi!'), production_standard_uniformity_details: Yup.object({ - target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'), - max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'), - min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'), - feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'), + target_mean_bw: Yup.number().required('Wajib diisi!'), + max_depletion: Yup.number().required('Wajib diisi!'), + min_uniformity: Yup.number().required('Wajib diisi!'), + feed_intake: Yup.number().required('Wajib diisi!'), }), production_standard_details: Yup.object({ - target_hen_day_production: Yup.number().required( - 'Produksi telur per hari wajib diisi!' - ), - target_hen_house_production: Yup.number().required( - 'Produksi telur per kandang wajib diisi!' - ), - target_egg_weight: Yup.number().required('Berat telur wajib diisi!'), - target_egg_mass: Yup.number().required('Massa telur wajib diisi!'), - standard_fcr: Yup.number().required('FCR wajib diisi!'), + target_hen_day_production: Yup.number().required('Wajib diisi!'), + target_hen_house_production: Yup.number().required('Wajib diisi!'), + target_egg_weight: Yup.number().required('Wajib diisi!'), + target_egg_mass: Yup.number().required('Wajib diisi!'), + standard_fcr: Yup.number().required('Wajib diisi!'), }).required(), }); // Schema for GROWING category (production_standard_details is optional) const GrowingRepeaterFormSchema = Yup.object({ - week: Yup.number().required('Minggu wajib diisi!'), + week: Yup.number().required('Wajib diisi!'), production_standard_uniformity_details: Yup.object({ - target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'), - max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'), - min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'), - feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'), + target_mean_bw: Yup.number().required('Wajib diisi!'), + max_depletion: Yup.number().required('Wajib diisi!'), + min_uniformity: Yup.number().required('Wajib diisi!'), + feed_intake: Yup.number().required('Wajib diisi!'), }), production_standard_details: Yup.object({ target_hen_day_production: Yup.number().optional(), diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx index 8dfc5f45..4512f474 100644 --- a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx +++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx @@ -344,7 +344,7 @@ const ProductionStandardForm = ({ const columns = useMemo[]>(() => { const baseColumns: ColumnDef[] = [ { - header: 'Minggu', + header: 'Week', accessorKey: 'week', enableSorting: false, }, @@ -358,30 +358,40 @@ const ProductionStandardForm = ({ header: 'Hen Day', accessorFn: (row) => row.production_standard_details?.target_hen_day_production, + cell: ({ row }) => + `${row.original.production_standard_details?.target_hen_day_production}%`, enableSorting: false, }, { header: 'Hen House', accessorFn: (row) => row.production_standard_details?.target_hen_house_production, + cell: ({ row }) => + `${row.original.production_standard_details?.target_hen_house_production} pc`, enableSorting: false, }, { header: 'Egg Weight', accessorFn: (row) => row.production_standard_details?.target_egg_weight, + cell: ({ row }) => + `${row.original.production_standard_details?.target_egg_weight} g`, enableSorting: false, }, { header: 'Egg Mass', accessorFn: (row) => row.production_standard_details?.target_egg_mass, + cell: ({ row }) => + `${row.original.production_standard_details?.target_egg_mass} g`, enableSorting: false, }, { header: 'FCR', accessorFn: (row) => row.production_standard_details?.standard_fcr, + cell: ({ row }) => + `${row.original.production_standard_details?.standard_fcr} g`, enableSorting: false, }, ] @@ -393,24 +403,32 @@ const ProductionStandardForm = ({ header: 'Mean BW', accessorFn: (row) => row.production_standard_uniformity_details?.target_mean_bw, + cell: ({ row }) => + `${row.original.production_standard_uniformity_details?.target_mean_bw} g`, enableSorting: false, }, { header: 'Max Depletion', accessorFn: (row) => row.production_standard_uniformity_details?.max_depletion, + cell: ({ row }) => + `${row.original.production_standard_uniformity_details?.max_depletion}%`, enableSorting: false, }, { header: 'Min Uniformity', accessorFn: (row) => row.production_standard_uniformity_details?.min_uniformity, + cell: ({ row }) => + `${row.original.production_standard_uniformity_details?.min_uniformity}%`, enableSorting: false, }, { header: 'Feed Intake', accessorFn: (row) => row.production_standard_uniformity_details?.feed_intake, + cell: ({ row }) => + `${row.original.production_standard_uniformity_details?.feed_intake} g`, enableSorting: false, }, ]; @@ -728,7 +746,52 @@ const ProductionStandardForm = ({ }; // ===== Formik Error List ===== - const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + const { formErrorList, close, handleFormSubmit } = useFormikErrorList( + formik, + { + onBeforeSubmit: (e) => { + e.preventDefault(); + + // For GROWING category, clear production_standard_details errors and set default values + if (formik.values.project_category === 'GROWING') { + // Set default values for production_standard_details + formik.values.details?.forEach((detail) => { + detail.production_standard_details = { + target_hen_day_production: 0, + target_hen_house_production: 0, + target_egg_weight: 0, + target_egg_mass: 0, + standard_fcr: 0, + }; + }); + + // Clear any errors related to production_standard_details + const currentErrors = { ...formik.errors }; + if (currentErrors.details && Array.isArray(currentErrors.details)) { + const cleanedDetails = currentErrors.details + .map((detailError) => { + if (detailError && typeof detailError === 'object') { + const { production_standard_details, ...rest } = detailError; + return Object.keys(rest).length > 0 ? rest : undefined; + } + return detailError; + }) + .filter( + (error): error is Exclude => + error !== undefined + ); + + currentErrors.details = ( + cleanedDetails.length > 0 ? cleanedDetails : undefined + ) as typeof currentErrors.details; + } + formik.setErrors(currentErrors); + } + + return true; + }, + } + ); return ( <> @@ -821,19 +884,20 @@ const ProductionStandardForm = ({ key={`row-${row.index}`} className='sticky bottom-0 bg-base-100 shadow-lg' > -
+
@@ -523,7 +581,9 @@ const DebtSupplierTab = () => {
+
} + bottomLabel='Persen (%)' errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -894,11 +958,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- Butir -
- } + bottomLabel='Butir (pc)' errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -930,11 +990,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr -
- } + bottomLabel='Gram (g)' errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -959,17 +1015,13 @@ const ProductionStandardForm = ({ name='production_standard_details.target_egg_mass' label='Egg Mass' placeholder='1' + bottomLabel='Gram (g)' value={ repeaterFormik.values .production_standard_details?.target_egg_mass } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr -
- } errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -1000,11 +1052,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr -
- } + bottomLabel='Gram (g)' errorMessage={getProductionDetailsError( repeaterFormik.errors .production_standard_details, @@ -1038,11 +1086,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr -
- } + bottomLabel='Gram (g)' errorMessage={ repeaterFormik.errors .production_standard_uniformity_details @@ -1072,7 +1116,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={} + bottomLabel='Persen (%)' errorMessage={ repeaterFormik.errors .production_standard_uniformity_details @@ -1102,7 +1146,7 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={} + bottomLabel='Persen (%)' errorMessage={ repeaterFormik.errors .production_standard_uniformity_details @@ -1132,11 +1176,8 @@ const ProductionStandardForm = ({ } onChange={repeaterFormik.handleChange} onBlur={repeaterFormik.handleBlur} - endAdornment={ -
- gr/ekor -
- } + bottomLabel='Gram/Ekor (g)' + endAdornment errorMessage={ repeaterFormik.errors .production_standard_uniformity_details @@ -1162,7 +1203,7 @@ const ProductionStandardForm = ({ type='button' color='error' variant='outline' - className='min-w-24' + className='min-w-xs' onClick={handleCancelEdit} > Batal @@ -1178,7 +1219,7 @@ const ProductionStandardForm = ({