From a781431683b2484a3e615c114aafc52159b98847 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 13 Jan 2026 13:35:56 +0700 Subject: [PATCH 01/49] refactor(FE): Rename Type column header to Jenis --- src/components/pages/closing/ClosingFinanceTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/closing/ClosingFinanceTable.tsx b/src/components/pages/closing/ClosingFinanceTable.tsx index 8c39b3fb..7eb34369 100644 --- a/src/components/pages/closing/ClosingFinanceTable.tsx +++ b/src/components/pages/closing/ClosingFinanceTable.tsx @@ -299,7 +299,7 @@ const ClosingFinanceTable = ({ }, }, { - header: 'Type', + header: 'Jenis', enableSorting: false, accessorFn: (item) => formatTitleCase(item.type || '-'), }, From 502564da0a0d1e36623531d6eb9cc9f7def63ac9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 13 Jan 2026 13:43:34 +0700 Subject: [PATCH 02/49] refactor(FE): Exclude step 6 and include step 4 in edit check --- src/app/expense/detail/edit/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/expense/detail/edit/page.tsx b/src/app/expense/detail/edit/page.tsx index e254f01d..2fb33484 100644 --- a/src/app/expense/detail/edit/page.tsx +++ b/src/app/expense/detail/edit/page.tsx @@ -38,9 +38,11 @@ const ExpenseEditPage = () => { !isLoadingExpense && isResponseSuccess(expense) && expense.data.latest_approval.step_number !== 5 && + expense.data.latest_approval.step_number !== 6 && (expense.data.latest_approval.step_number === 1 || expense.data.latest_approval.step_number === 2 || - expense.data.latest_approval.step_number === 3); + expense.data.latest_approval.step_number === 3 || + expense.data.latest_approval.step_number === 4); if (!isLoadingExpense && !isExpenseCanBeEdited) { router.back(); From 92bfef850a9eb7f10888b549a7a0600e6184c3d6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 13 Jan 2026 14:31:30 +0700 Subject: [PATCH 03/49] refactor(FE): Enhance customer payment PDF export and filters --- .../export/CustomerPaymentExportPDF.tsx | 145 +++++++++++++++--- .../report/finance/tab/CustomerPaymentTab.tsx | 52 +++---- 2 files changed, 145 insertions(+), 52 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 88c556de..08efa409 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -136,41 +136,129 @@ const pdfStyles = StyleSheet.create({ backgroundColor: '#F0F0F0', fontWeight: 'bold', }, + badge: { + backgroundColor: '#1f74bf', + color: '#FFFFFF', + padding: 2, + borderRadius: 2, + fontSize: 7, + fontWeight: 'bold', + alignSelf: 'center', + marginRight: 4, + }, + badgeLunas: { + backgroundColor: '#1f74bf', + color: '#FFFFFF', + }, + badgeBelumLunas: { + backgroundColor: '#F97316', + color: '#FFFFFF', + }, + parameterBadge: { + backgroundColor: '#F5F5F5', + color: '#333333', + padding: 4, + borderRadius: 4, + fontSize: 8, + marginRight: 8, + marginBottom: 4, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, }); interface CustomerPaymentExportPDFParams { data: CustomerPaymentReport[]; + params?: { + customer_name?: string; + sales?: string; + start_date?: string; + end_date?: string; + filter_by?: string; + }; } +const getParameterText = ( + params?: CustomerPaymentExportPDFParams['params'] +) => { + const paramsText = []; + + if (params?.customer_name) { + paramsText.push(`Customer: ${params.customer_name}`); + } else { + paramsText.push('Semua Customer'); + } + + if (params?.sales) { + paramsText.push(`Sales: ${params.sales}`); + } + + if (params?.start_date && params?.end_date) { + const startDate = formatDate(params.start_date, 'DD MMM YYYY'); + const endDate = formatDate(params.end_date, 'DD MMM YYYY'); + paramsText.push(`Periode: ${startDate} - ${endDate}`); + } else if (params?.start_date) { + const startDate = formatDate(params.start_date, 'DD MMM YYYY'); + paramsText.push(`Tanggal: ${startDate}`); + } + + const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm'); + paramsText.push(`Dicetak: ${currentDate}`); + + return paramsText; +}; + const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { return ( {params.data.map((customerReport, customerIndex) => ( - {/* Title and Customer Info */} + {/* Title and Parameters */} Laporan > Kontrol Pembayaran Customer + + + + Periode:{' '} + {params.params?.start_date + ? formatDate(params.params.start_date, 'DD MMM YYYY') + : '-'}{' '} + s.d{' '} + {params.params?.end_date + ? formatDate(params.params.end_date, 'DD MMM YYYY') + : '-'} + + + + Filter Tanggal: Tanggal DO + + + + Customer: {params.params?.customer_name || 'Semua Customer'} + + + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + + {customerReport.customer.name} - {customerReport.customer.address || ''} + Alamat: {customerReport.customer.address || '-'} - {customerReport.summary && ( - - Total Saldo Piutang:{' '} - {formatCurrency( - customerReport.summary.total_accounts_receivable - )} - - )} {/* Table */} @@ -181,10 +269,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { No - Tgl DO/Bayar + Tanggal DO - Tgl Realisasi + Tanggal Realisasi Aging @@ -193,16 +281,16 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { Referensi - No. Polisi + No Polisi Qty - Berat (Kg) + Berat - AVG + Rata-Rata Harga Awal @@ -214,7 +302,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { Harga Akhir - PPN (%) + Pajak Total @@ -223,10 +311,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { Pembayaran - Saldo Piutang + Saldo - Ket + Keterangan Pengambilan @@ -304,7 +392,24 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {formatCurrency(item.accounts_receivable)} - {item.notes || '-'} + {item.notes ? ( + {item.notes} + ) : ( + + + {item.accounts_receivable === 0 + ? 'Lunas' + : 'Belum Lunas'} + + + )} {item.pickup_info || '-'} diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 1d8d1993..6cd0ba3d 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -46,7 +46,6 @@ const CustomerPaymentTab = () => { const [filterSales, setFilterSales] = useState([]); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); - const [filterErrors, setFilterErrors] = useState>({}); const filterModal = useModal(); @@ -75,27 +74,13 @@ const CustomerPaymentTab = () => { setFilterSales([]); setFilterStartDate(''); setFilterEndDate(''); - setFilterErrors({}); }, []); const handleApplyFilters = useCallback(() => { - const errors: Record = {}; - - if (!filterStartDate) { - errors.start_date = 'Tanggal mulai wajib diisi'; - } - if (!filterEndDate) { - errors.end_date = 'Tanggal akhir wajib diisi'; - } - - setFilterErrors(errors); - - if (Object.keys(errors).length === 0) { - setIsSubmitted(true); - setCurrentPage(1); - filterModal.closeModal(); - } - }, [filterModal, filterStartDate, filterEndDate]); + setIsSubmitted(true); + setCurrentPage(1); + filterModal.closeModal(); + }, [filterModal]); // ===== DATA FETCHING ===== const { data: customerPayment, isLoading } = useSWR( @@ -218,7 +203,22 @@ const CustomerPaymentTab = () => { return; } - await generateCustomerPaymentPDF({ data: allDataForExport }); + await generateCustomerPaymentPDF({ + data: allDataForExport, + params: { + customer_name: + filterCustomer.length > 0 + ? filterCustomer.map((c) => c.label).join(', ') + : undefined, + sales: + filterSales.length > 0 + ? filterSales.map((s) => s.label).join(', ') + : undefined, + start_date: filterStartDate || undefined, + end_date: filterEndDate || undefined, + filter_by: 'do_date', + }, + }); toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); @@ -538,15 +538,9 @@ const CustomerPaymentTab = () => { value={filterStartDate} onChange={(e) => { setFilterStartDate(e.target.value); - setFilterErrors((prev) => ({ ...prev, start_date: '' })); }} className={{ wrapper: 'w-full' }} /> - {filterErrors.start_date && ( -

- {filterErrors.start_date} -

- )}
@@ -556,15 +550,9 @@ const CustomerPaymentTab = () => { value={filterEndDate} onChange={(e) => { setFilterEndDate(e.target.value); - setFilterErrors((prev) => ({ ...prev, end_date: '' })); }} className={{ wrapper: 'w-full' }} /> - {filterErrors.end_date && ( -

- {filterErrors.end_date} -

- )}
From 60eaec261d8851ec3c40daab8b250eb85b7380ec Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 13 Jan 2026 15:20:27 +0700 Subject: [PATCH 04/49] refactor(FE): Render payment notes as Badge and highlight balance --- .../report/finance/tab/CustomerPaymentTab.tsx | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 6cd0ba3d..2b2c09a2 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -2,6 +2,7 @@ import { useState, useMemo, useCallback } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Card from '@/components/Card'; +import Badge from '@/components/Badge'; import SelectInput, { useSelect, OptionType, @@ -67,6 +68,38 @@ const CustomerPaymentTab = () => { [] ); + const getPaymentStatusColor = (notes: string) => { + const normalizedValue = notes.toLowerCase(); + + if (normalizedValue === 'lunas') { + return 'bg-info/10 text-info border-info'; + } + + if (normalizedValue.includes('belum')) { + return 'bg-warning/10 text-warning border-warning'; + } + + return 'bg-gray-100 text-gray-600 border-gray-300'; + }; + + const getPaymentStatusIndicatorColor = (notes: string) => { + const normalizedValue = notes.toLowerCase(); + + if (normalizedValue === 'lunas') { + return 'bg-info'; + } + + if (normalizedValue.includes('belum')) { + return 'bg-warning'; + } + + return 'bg-gray-400'; + }; + + const getPaymentStatusText = (notes: string) => { + return notes; + }; + // ===== FILTER HANDLERS ===== const handleResetFilters = useCallback(() => { setIsSubmitted(false); @@ -435,7 +468,9 @@ const CustomerPaymentTab = () => { accessorKey: 'accounts_receivable', cell: (props) => { const value = props.row.original.accounts_receivable; - return
{formatCurrency(value)}
; + return ( +
{formatCurrency(value)}
+ ); }, footer: () => (
@@ -449,7 +484,23 @@ const CustomerPaymentTab = () => { accessorKey: 'notes', cell: (props) => { const value = props.row.original.notes; - return value || '-'; + + if (!value) { + return '-'; + } + + return ( + + {getPaymentStatusText(value)} + + ); }, }, { @@ -647,14 +698,13 @@ const CustomerPaymentTab = () => { total_accounts_receivable: 0, }; - const totalAccountsReceivable = summary.total_accounts_receivable; const tableColumns = getTableColumns(summary); return ( Date: Tue, 13 Jan 2026 15:32:39 +0700 Subject: [PATCH 05/49] refactor(FE): Style accounts receivable in red and adjust badge --- .../report/finance/export/CustomerPaymentExportPDF.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx index 08efa409..5adcb694 100644 --- a/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx +++ b/src/components/pages/report/finance/export/CustomerPaymentExportPDF.tsx @@ -154,6 +154,9 @@ const pdfStyles = StyleSheet.create({ backgroundColor: '#F97316', color: '#FFFFFF', }, + textError: { + color: '#DC2626', + }, parameterBadge: { backgroundColor: '#F5F5F5', color: '#333333', @@ -389,7 +392,9 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { {formatCurrency(item.payment)} - {formatCurrency(item.accounts_receivable)} + + {formatCurrency(item.accounts_receivable)} + {item.notes ? ( @@ -483,7 +488,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => { - + {formatCurrency( customerReport.summary.total_accounts_receivable )} From e77a43300a5a42663b32d1852c7956a1a9103739 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 13 Jan 2026 16:01:30 +0700 Subject: [PATCH 06/49] refactor(FE): Nest project_flock in Recording and update UI --- .../production/recording/RecordingTable.tsx | 8 +- .../recording/form/RecordingForm.tsx | 92 ++++++++++++------- src/types/api/production/recording.d.ts | 44 ++++++--- 3 files changed, 98 insertions(+), 46 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 39b17ef7..6b10b26e 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -678,12 +678,13 @@ const RecordingTable = () => { { header: 'Nama Project', cell: (props) => - `Project ${props.row.original.project_flock_kandang_id}`, + props.row.original.project_flock?.flock_name || '-', }, { header: 'Kategori', cell: (props) => { - const category = props.row.original.project_flock_category; + const category = + props.row.original.project_flock?.project_flock_category; if (!category) return '-'; const color = category === 'LAYING' ? 'info' : 'warning'; return ( @@ -706,7 +707,8 @@ const RecordingTable = () => { { header: 'Populasi Awal', cell: (props) => - props.row.original.total_chick_qty?.toLocaleString() || '-', + props.row.original.project_flock?.total_chick_qty?.toLocaleString() || + '-', }, { header: 'Status Approval', diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 4966172c..5097fe27 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -252,9 +252,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { : undefined; const projectFlockKandangDetailUrl = useMemo(() => { - if (type === 'add' || !initialValues?.project_flock_kandang_id) return null; - return `${ProjectFlockKandangApi.basePath}/${initialValues.project_flock_kandang_id}`; - }, [type, initialValues?.project_flock_kandang_id]); + if ( + type === 'add' || + !initialValues?.project_flock?.project_flock_kandang_id + ) + return null; + return `${ProjectFlockKandangApi.basePath}/${initialValues.project_flock.project_flock_kandang_id}`; + }, [type, initialValues?.project_flock?.project_flock_kandang_id]); const { data: projectFlockKandangDetailData } = useSWR( projectFlockKandangDetailUrl, @@ -404,12 +408,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [approvedProjectFlockKandangsData]); const isLayingCategory = - initialValues?.project_flock_category === 'LAYING' || + initialValues?.project_flock?.project_flock_category === 'LAYING' || projectFlockKandangLookup?.project_flock?.category === 'LAYING' || projectFlockKandangDetail?.project_flock?.category === 'LAYING'; const isGrowingCategory = - initialValues?.project_flock_category === 'GROWING' || + initialValues?.project_flock?.project_flock_category === 'GROWING' || projectFlockKandangLookup?.project_flock?.category === 'GROWING' || projectFlockKandangDetail?.project_flock?.category === 'GROWING'; @@ -555,7 +559,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { todayRecordings.forEach((recording) => { const recordingDate = recording.record_datetime?.split('T')[0]; if (recordingDate === today) { - recordedIds.add(recording.project_flock_kandang_id); + recordedIds.add(recording.project_flock.project_flock_kandang_id); } }); @@ -1005,7 +1009,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const hasSameDayRecording = isResponseSuccess(existingRecordings) ? existingRecordings.data?.some( (recording: Recording) => - recording.project_flock_kandang_id === + recording.project_flock.project_flock_kandang_id === projectFlockKandangId && recording.day === nextDayRecording.next_day ) @@ -1543,13 +1547,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - {initialValues.project_flock_category} + {initialValues.project_flock?.project_flock_category}

@@ -1579,7 +1584,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {type === 'detail' && initialValues && (
{ - {initialValues.fcr_std && initialValues.fcr_std > 0 - ? formatNumber(initialValues.fcr_std) + {initialValues.project_flock?.fcr?.fcr_std && + initialValues.project_flock?.fcr?.fcr_std > 0 + ? formatNumber( + initialValues.project_flock?.fcr?.fcr_std + ) : '-'} @@ -1630,9 +1638,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - {initialValues.feed_intake_std && - initialValues.feed_intake_std > 0 - ? formatNumber(initialValues.feed_intake_std) + {initialValues.project_flock?.production_standart + ?.feed_intake_std && + initialValues.project_flock?.production_standart + ?.feed_intake_std > 0 + ? formatNumber( + initialValues.project_flock?.production_standart + ?.feed_intake_std + ) : '-'} @@ -1698,9 +1711,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { colSpan={2} className='text-center py-3 font-semibold' > - {initialValues.total_chick_qty && - initialValues.total_chick_qty > 0 - ? formatNumber(initialValues.total_chick_qty) + {initialValues.project_flock?.total_chick_qty && + initialValues.project_flock?.total_chick_qty > 0 + ? formatNumber( + initialValues.project_flock?.total_chick_qty + ) : '-'} @@ -1712,7 +1727,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Egg Production Section - Only for LAYING category */} {type === 'detail' && initialValues && - initialValues.project_flock_category === 'LAYING' && ( + initialValues.project_flock?.project_flock_category === + 'LAYING' && (
@@ -1744,9 +1760,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - {initialValues.egg_mass_std && - initialValues.egg_mass_std > 0 - ? formatNumber(initialValues.egg_mass_std) + {initialValues.project_flock?.production_standart + ?.egg_mass_std && + initialValues.project_flock?.production_standart + ?.egg_mass_std > 0 + ? formatNumber( + initialValues.project_flock + ?.production_standart?.egg_mass_std + ) : '-'} @@ -1763,9 +1784,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - {initialValues.egg_weight_std && - initialValues.egg_weight_std > 0 - ? formatNumber(initialValues.egg_weight_std) + {initialValues.project_flock?.production_standart + ?.egg_weight_std && + initialValues.project_flock?.production_standart + ?.egg_weight_std > 0 + ? formatNumber( + initialValues.project_flock + ?.production_standart?.egg_weight_std + ) : '-'} @@ -1780,9 +1806,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - {initialValues.hen_day_std !== undefined && - initialValues.hen_day_std > 0 - ? `${initialValues.hen_day_std}%` + {initialValues.project_flock?.production_standart + ?.hen_day_std !== undefined && + initialValues.project_flock?.production_standart + ?.hen_day_std > 0 + ? `${initialValues.project_flock?.production_standart?.hen_day_std}%` : '-'} @@ -1797,9 +1825,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { - {initialValues.hen_house_std !== undefined && - initialValues.hen_house_std > 0 - ? `${initialValues.hen_house_std}%` + {initialValues.project_flock?.production_standart + ?.hen_house_std !== undefined && + initialValues.project_flock?.production_standart + ?.hen_house_std > 0 + ? `${initialValues.project_flock?.production_standart?.hen_house_std}%` : '-'} diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 1728516a..079bf045 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -1,34 +1,52 @@ import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; +import { Warehouse } from '@/types/api/master-data/warehouse'; + +export type ProductionStandard = { + id: number; + week: number; + name: string; + hen_day_std: number; + hen_house_std: number; + feed_intake_std: number; + max_depletion_std: number; + egg_mass_std: number; + egg_weight_std: number; +}; + +export type FCR = { + id: number; + name: string; + fcr_std: number; +}; + +export type ProjectFlock = { + project_flock_kandang_id: number; + flock_name: string; + project_flock_category: 'GROWING' | 'LAYING'; + period: number; + production_standart: ProductionStandard; + fcr: FCR; + total_chick_qty: number; +}; export type ProductionMetrics = { total_depletion_qty: number; cum_depletion_rate: number; cum_intake: number; fcr_value: number; - fcr_std?: number; - total_chick_qty: number; hen_day?: number; hen_house?: number; feed_intake?: number; - feed_intake_std?: number; egg_mass?: number; egg_weight?: number; - hen_day_std?: number; - hen_house_std?: number; - egg_mass_std?: number; - egg_weight_std?: number; - daily_gain?: number; - avg_daily_gain?: number; - cum_depletion?: number; }; export type BaseRecording = { id: number; - project_flock_kandang_id: number; + project_flock: ProjectFlock; record_datetime: string; day: number; - project_flock_category?: 'GROWING' | 'LAYING'; } & ProductionMetrics; export type RecordingDepletion = { @@ -68,6 +86,8 @@ export type Recording = BaseMetadata & BaseRecording & { approval?: BaseApproval; created_user: User; + warehouse?: Warehouse; + product_category?: 'GROWING' | 'LAYING'; depletions?: RecordingDepletion[]; stocks?: RecordingStock[]; eggs?: RecordingEgg[]; From 8481b77c90d38b061440a9a602e519b7d1d35fd3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 13 Jan 2026 16:21:41 +0700 Subject: [PATCH 07/49] refactor(FE): Adjust cumulative depletion table layout --- .../recording/form/RecordingForm.tsx | 56 ++++++------------- 1 file changed, 17 insertions(+), 39 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 5097fe27..c279bde7 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1663,54 +1663,31 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
- - - - - - - - - - + - + + + + - - - - - + + +
- DEPLESI KUMULATIF -
- Total - - (%) -
+ Deplesi Kumulatif - {initialValues.total_depletion_qty && - initialValues.total_depletion_qty > 0 - ? formatNumber(initialValues.total_depletion_qty) + {initialValues.cum_depletion_rate && + initialValues.cum_depletion_rate > 0 + ? `${initialValues.cum_depletion_rate.toFixed(2)}%` : '-'} - {initialValues.cum_depletion_rate && - initialValues.cum_depletion_rate > 0 - ? initialValues.cum_depletion_rate.toFixed(2) +
Total Depletion + {initialValues.total_depletion_qty && + initialValues.total_depletion_qty > 0 + ? formatNumber(initialValues.total_depletion_qty) : '-'}
- Total Ayam -
+
Total Ayam {initialValues.project_flock?.total_chick_qty && initialValues.project_flock?.total_chick_qty > 0 ? formatNumber( @@ -1718,6 +1695,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ) : '-'}
From 582b971c09b6aa394ba5dbbc3ed6db435ad6d5ce Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 13 Jan 2026 16:47:58 +0700 Subject: [PATCH 08/49] refactor(FE): Change endpoint path to reports --- src/components/pages/report/MarketingReportContent.tsx | 2 +- src/services/api/report/marketing-sale.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/pages/report/MarketingReportContent.tsx b/src/components/pages/report/MarketingReportContent.tsx index d54c935a..3ebacecb 100644 --- a/src/components/pages/report/MarketingReportContent.tsx +++ b/src/components/pages/report/MarketingReportContent.tsx @@ -33,7 +33,7 @@ const MarketingReportContent = () => { const [activeTab, setActiveTab] = useState('daily'); return ( -
+
Date: Wed, 14 Jan 2026 00:02:44 +0700 Subject: [PATCH 09/49] refactor(FE): Add record_date to recording payloads --- .../pages/production/recording/form/RecordingForm.tsx | 4 ++++ src/types/api/production/recording.d.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index c279bde7..4f9018fc 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -117,8 +117,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { // ===== PAYLOAD CREATION HELPERS ===== const createGrowingPayload = useCallback( (values: RecordingGrowingFormValues) => { + const today = new Date().toISOString().split('T')[0]; return { project_flock_kandang_id: values.project_flock_kandang_id, + record_date: today, stocks: (values.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, qty: Number(stock.qty) || 0, @@ -134,8 +136,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const createLayingPayload = useCallback( (values: RecordingLayingFormValues) => { + const today = new Date().toISOString().split('T')[0]; return { project_flock_kandang_id: values.project_flock_kandang_id, + record_date: today, stocks: (values.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, qty: Number(stock.qty) || 0, diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 079bf045..e30abe4c 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -101,6 +101,7 @@ export type NextDayRecording = { export type CreateGrowingRecordingPayload = { project_flock_kandang_id: number; + record_date: string; stocks?: { product_warehouse_id: number; qty: number; From a5d2d855725b704cc606b5264ab696d0dc72e051 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 14 Jan 2026 09:28:16 +0700 Subject: [PATCH 10/49] chore: hide AWG std and AWG act --- .../pages/closing/ClosingProductionDataTabContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx index aabf48de..0f15d5b9 100644 --- a/src/components/pages/closing/ClosingProductionDataTabContent.tsx +++ b/src/components/pages/closing/ClosingProductionDataTabContent.tsx @@ -197,7 +197,7 @@ const ClosingProductionDataTabContent = ({ value={formatNumber(performance.mor_diff)} unitClassName='hidden' /> - + /> */} Date: Wed, 14 Jan 2026 09:28:47 +0700 Subject: [PATCH 11/49] feat: date filter type --- .../report/DailyMarketingReportContent.tsx | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/DailyMarketingReportContent.tsx index 3ddbd6cf..d17df01e 100644 --- a/src/components/pages/report/DailyMarketingReportContent.tsx +++ b/src/components/pages/report/DailyMarketingReportContent.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useState } from 'react'; +import { ChangeEventHandler, useEffect, useState } from 'react'; import { pdf } from '@react-pdf/renderer'; import toast from 'react-hot-toast'; @@ -28,7 +28,10 @@ import { import { Warehouse } from '@/types/api/master-data/warehouse'; import { Customer } from '@/types/api/master-data/customer'; import { MarketingReportApi } from '@/services/api/report/marketing-report'; -import { MARKETING_TYPE_OPTIONS } from '@/config/constant'; +import { + MARKETING_DATE_FILTER_TYPE_OPTIONS, + MARKETING_TYPE_OPTIONS, +} from '@/config/constant'; import { httpClient } from '@/services/http/client'; import { BaseApiResponse } from '@/types/api/api-general'; import { @@ -150,6 +153,15 @@ const DailyMarketingReportContent = () => { updateFilter('end_date', e.target.value ? e.target.value : ''); }; + const [selectedMarketingDateFilterType, setSelectedMarketingDateFilterType] = + useState(null); + const marketingDateFilterTypeChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + setSelectedMarketingDateFilterType(val as OptionType); + updateFilter('filter_by', val ? ((val as OptionType).value as string) : ''); + }; + const [selectedMarketingType, setSelectedMarketingType] = useState(null); const marketingTypeChangeHandler = ( @@ -252,6 +264,23 @@ const DailyMarketingReportContent = () => { resetFilter(); }; + useEffect(() => { + if ( + tableFilterState.filter_by === 'realization_date' || + tableFilterState.filter_by === 'so_date' + ) { + setSelectedMarketingDateFilterType({ + label: + tableFilterState.filter_by === 'realization_date' + ? 'Tanggal Realisasi' + : 'Tanggal SO', + value: tableFilterState.filter_by, + }); + } else { + setSelectedMarketingDateFilterType(null); + } + }, [tableFilterState.filter_by]); + return (
@@ -341,6 +370,18 @@ const DailyMarketingReportContent = () => {
+ + Date: Wed, 14 Jan 2026 09:29:15 +0700 Subject: [PATCH 12/49] chore: adjust table columns accessorKey for filter --- .../pages/report/DailyMarketingsTable.tsx | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx index 94da96f8..2dba0309 100644 --- a/src/components/pages/report/DailyMarketingsTable.tsx +++ b/src/components/pages/report/DailyMarketingsTable.tsx @@ -71,19 +71,22 @@ const DailyMarketingsTable = ({ cell: (props) => `${props.row.original.aging_days} hari`, }, { - accessorKey: 'warehouse.name', + accessorKey: 'warehouse', header: 'Gudang', + cell: ({ row }) => row.original.warehouse.name, }, { - accessorKey: 'customer.name', + accessorKey: 'customer', header: 'Pelanggan', + cell: ({ row }) => row.original.customer.name, }, { accessorKey: 'do_number', header: 'No. DO', + enableSorting: false, }, { - accessorKey: 'sales', + accessorKey: 'sales_person', header: 'Sales/Marketing', cell: (props) => props.row.original.sales.name, }, @@ -97,10 +100,12 @@ const DailyMarketingsTable = ({ { accessorKey: 'marketing_type', header: 'Marketing Type', + enableSorting: false, }, { - accessorKey: 'product.name', + accessorKey: 'product', header: 'Produk', + cell: ({ row }) => row.original.product.name, }, { accessorKey: 'qty', @@ -115,12 +120,12 @@ const DailyMarketingsTable = ({ }, }, { - accessorKey: 'average_weight_kg', + accessorKey: 'average_weight', header: 'Bobot Rata-Rata (Kg)', cell: (props) => formatNumber(props.row.original.average_weight_kg), }, { - accessorKey: 'total_weight_kg', + accessorKey: 'total_weight', header: 'Bobot Total (Kg)', cell: (props) => formatNumber(props.row.original.total_weight_kg), footer: () => { @@ -132,12 +137,12 @@ const DailyMarketingsTable = ({ }, }, { - accessorKey: 'sales_price_per_kg', + accessorKey: 'sales_price', header: 'Harga Jual (Rp)', cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), }, { - accessorKey: 'hpp_price_per_kg', + accessorKey: 'hpp_price', header: 'HPP (Rp)', cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), footer: () => { @@ -163,6 +168,8 @@ const DailyMarketingsTable = ({ ]; useEffect(() => { + console.log({ sorting }); + if (sorting.length === 1) { onFilterByChange(sorting[0].id); onSortByChange(sorting[0].desc ? 'desc' : 'asc'); From c4b505047cd656456c6bc62bd0245afc0859a20e Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 14 Jan 2026 09:29:27 +0700 Subject: [PATCH 13/49] chore: add MARKETING_DATE_FILTER_TYPE_OPTIONS constant --- src/config/constant.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index 364f1824..d3832613 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -457,3 +457,14 @@ export const MARKETING_TYPE_OPTIONS = [ value: 'trading', }, ]; + +export const MARKETING_DATE_FILTER_TYPE_OPTIONS = [ + { + label: 'Tanggal Realisasi', + value: 'realization_date', + }, + { + label: 'Tanggal SO', + value: 'so_date', + }, +]; From adb8d0f69e9f455dba5172277b1e56dcca369c2e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 14 Jan 2026 10:04:51 +0700 Subject: [PATCH 14/49] feat(FE): Add checkbox multi-select and components prop --- src/components/input/SelectInput.tsx | 23 ++++-- src/components/input/SelectInputCheckbox.tsx | 82 +++++++++++++++++++ .../report/finance/tab/CustomerPaymentTab.tsx | 4 +- 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 src/components/input/SelectInputCheckbox.tsx diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index d35e7589..e3dbc011 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -35,6 +35,7 @@ interface SelectInputBaseProps { bottomLabel?: ReactNode; options: T[]; optionComponent?: OptionComponent; + components?: Partial; isDisabled?: boolean; isLoading?: boolean; isClearable?: boolean; @@ -56,9 +57,12 @@ interface SelectInputBaseProps { onInputChange?: (search: string) => void; startAdornment?: ReactNode; menuPortalTarget?: HTMLElement | null; + closeMenuOnSelect?: boolean; + hideSelectedOptions?: boolean; } -interface SelectInputProps extends SelectInputBaseProps { +export interface SelectInputProps + extends SelectInputBaseProps { createables?: boolean; value?: T | T[] | null; onChange?: (val: T | T[] | null) => void; @@ -101,6 +105,7 @@ const SelectInput = (props: SelectInputProps) => { onChange, options, optionComponent, + components: customComponents, isDisabled, isLoading, isClearable, @@ -119,6 +124,8 @@ const SelectInput = (props: SelectInputProps) => { onInputChange, startAdornment, menuPortalTarget, + closeMenuOnSelect, + hideSelectedOptions, } = props; const [internalInputValue, setInternalInputValue] = useState(''); @@ -128,14 +135,18 @@ const SelectInput = (props: SelectInputProps) => { const components = useMemo(() => { const base = isAnimated ? animatedComponents : {}; - const customComponents = { ...base, IndicatorSeparator: () => null }; + const mergedComponents = { ...base, IndicatorSeparator: () => null }; if (startAdornment) { - customComponents.Control = CustomControl; + mergedComponents.Control = CustomControl; } - return customComponents; - }, [isAnimated, startAdornment]); + if (customComponents) { + Object.assign(mergedComponents, customComponents); + } + + return mergedComponents; + }, [isAnimated, startAdornment, customComponents]); const internalInputChangeHandler = (val: string, meta: InputActionMeta) => { if (meta.action === 'input-change') setInternalInputValue(val); @@ -205,6 +216,8 @@ const SelectInput = (props: SelectInputProps) => { isRtl={isRtl} isSearchable={isSearchable} placeholder={placeholder} + closeMenuOnSelect={closeMenuOnSelect} + hideSelectedOptions={hideSelectedOptions} className={cn('w-full', className?.select)} classNames={{ ...(!startAdornment && { diff --git a/src/components/input/SelectInputCheckbox.tsx b/src/components/input/SelectInputCheckbox.tsx new file mode 100644 index 00000000..0827a70a --- /dev/null +++ b/src/components/input/SelectInputCheckbox.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { useMemo } from 'react'; +import { + OptionProps, + GroupBase, + components as ReactSelectComponents, +} from 'react-select'; +import SelectInput, { OptionType, SelectInputProps } from './SelectInput'; +import { cn } from '@/lib/helper'; + +interface SelectInputCheckboxProps + extends Omit< + SelectInputProps, + 'closeMenuOnSelect' | 'hideSelectedOptions' | 'optionComponent' + > { + closeMenuOnSelect?: boolean; + hideSelectedOptions?: boolean; +} + +const CheckboxOption = < + T extends OptionType, + IsMulti extends boolean, + Group extends GroupBase, +>( + props: OptionProps +) => { + const { isSelected, label, innerRef, innerProps, className } = props; + + return ( +
+ null} + className='checkbox checkbox-sm checkbox-primary pointer-events-none' + /> + +
+ ); +}; + +const SelectInputCheckbox = ( + props: SelectInputCheckboxProps +) => { + const { + closeMenuOnSelect = false, + hideSelectedOptions = false, + isMulti = true, + className, + ...restProps + } = props; + + const customComponents = useMemo(() => { + return { + Option: CheckboxOption as typeof ReactSelectComponents.Option, + }; + }, []); + + return ( + + {...restProps} + isMulti={isMulti} + closeMenuOnSelect={closeMenuOnSelect} + hideSelectedOptions={hideSelectedOptions} + className={{ + ...className, + select: cn(className?.select, 'select-checkbox'), + }} + components={customComponents} + /> + ); +}; + +export default SelectInputCheckbox; diff --git a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx index 2b2c09a2..dfadd1ec 100644 --- a/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx +++ b/src/components/pages/report/finance/tab/CustomerPaymentTab.tsx @@ -7,6 +7,7 @@ import SelectInput, { useSelect, OptionType, } from '@/components/input/SelectInput'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; import DateInput from '@/components/input/DateInput'; import { CustomerApi } from '@/services/api/master-data'; import { FinanceApi } from '@/services/api/report/finance-report'; @@ -608,10 +609,9 @@ const CustomerPaymentTab = () => {
- { From 8d7adbbd27b4948ad86c842c5fd7f74d873ef723 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 14 Jan 2026 10:35:51 +0700 Subject: [PATCH 15/49] feat: implement lazy loading in SelectInput --- src/components/input/SelectInput.tsx | 136 ++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 21 deletions(-) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index d35e7589..74e23bc4 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -9,15 +9,20 @@ import Select, { SingleValue, components as ReactSelectComponents, ControlProps, + MenuListProps, } from 'react-select'; import CreatableSelect from 'react-select/creatable'; import makeAnimated from 'react-select/animated'; import { useDebounce } from 'use-debounce'; import { cn, getByPath } from '@/lib/helper'; -import useSWR from 'swr'; +import useSWRInfinite from 'swr/infinite'; import { httpClientFetcher } from '@/services/http/client'; -import { BaseApiResponse } from '@/types/api/api-general'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { + BaseApiResponse, + ErrorApiResponse, + SuccessApiResponse, +} from '@/types/api/api-general'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; export interface OptionType { value: string | number; @@ -56,6 +61,7 @@ interface SelectInputBaseProps { onInputChange?: (search: string) => void; startAdornment?: ReactNode; menuPortalTarget?: HTMLElement | null; + onMenuScrollToBottom?: ((event: WheelEvent | TouchEvent) => void) | undefined; } interface SelectInputProps extends SelectInputBaseProps { @@ -93,6 +99,29 @@ const CustomControl = < ); }; +const CustomMenuList = < + Option, + IsMulti extends boolean, + Group extends GroupBase
{type !== 'add' && ( From 395270464396925bb38f2af68e5b00a3fb8167e2 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 11:45:51 +0700 Subject: [PATCH 37/49] chore: fix typo in placeholder --- src/components/pages/master-data/kandang/form/KandangForm.tsx | 2 +- src/components/pages/master-data/nonstock/form/NonstockForm.tsx | 2 +- .../pages/master-data/warehouse/form/WarehouseForm.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/master-data/kandang/form/KandangForm.tsx b/src/components/pages/master-data/kandang/form/KandangForm.tsx index acced3c5..22ad91f8 100644 --- a/src/components/pages/master-data/kandang/form/KandangForm.tsx +++ b/src/components/pages/master-data/kandang/form/KandangForm.tsx @@ -215,7 +215,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { required label='Nama' name='name' - placeholder='Masukkan nama lokasi' + placeholder='Masukkan nama kandang' value={formik.values.name} onChange={formik.handleChange} onBlur={formik.handleBlur} diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index cd2c361b..4622a6a3 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -229,7 +229,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { required label='Nama' name='name' - placeholder='Masukkan nama lokasi' + placeholder='Masukkan nama nonstock' value={formik.values.name} onChange={formik.handleChange} onBlur={formik.handleBlur} diff --git a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx index cab9f750..a6a53e3f 100644 --- a/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx +++ b/src/components/pages/master-data/warehouse/form/WarehouseForm.tsx @@ -330,7 +330,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => { required label='Nama' name='name' - placeholder='Masukkan nama lokasi' + placeholder='Masukkan nama warehouse' value={formik.values.name} onChange={formik.handleChange} onBlur={formik.handleBlur} From 790b59066819aaccf4dbd931913c3da41ae75c8b Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 11:46:10 +0700 Subject: [PATCH 38/49] feat: use real permission for daily checklist --- src/config/route-permission.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 9a0c9d2e..165bc8ee 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -5,22 +5,22 @@ export const ROUTE_PERMISSIONS: Record = { '/dashboard/': ['lti.dashboard.list'], // Daily Checklist - // TODO: use real daily checklist permission name - // '/daily-checklist/': ['lti.daily_checklist.list'], - // '/daily-checklist/dashboard/': ['lti.daily_checklist.list'], - // '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'], - // '/daily-checklist/list-daily-checklist/detail/': ['lti.daily_checklist.detail'], - // '/daily-checklist/reports/': ['lti.daily_checklist.reports'], - // '/daily-checklist/master-data/employee/': ['lti.dashboard.master_data.employee'], - // '/daily-checklist/master-data/activity/': ['lti.dashboard.master_data.activity'], - '/daily-checklist/dashboard/': ['lti.dashboard.list'], - '/daily-checklist/daily-checklist/': ['lti.dashboard.list'], - '/daily-checklist/list-daily-checklist/': ['lti.dashboard.list'], - '/daily-checklist/list-daily-checklist/detail/': ['lti.dashboard.list'], - '/daily-checklist/reports/': ['lti.dashboard.list'], - '/daily-checklist/master-data/employee/': ['lti.dashboard.list'], - '/daily-checklist/master-data/activity/': ['lti.dashboard.list'], - '/daily-checklist/master-data/configuration/': ['lti.dashboard.list'], + '/daily-checklist/dashboard/': ['lti.daily_checklist.dashboard.list'], + '/daily-checklist/daily-checklist/': ['lti.daily_checklist.create'], + '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'], + '/daily-checklist/list-daily-checklist/detail/': [ + 'lti.daily_checklist.detail', + ], + '/daily-checklist/reports/': ['lti.daily_checklist.reports'], + '/daily-checklist/master-data/employee/': [ + 'lti.daily_checklist.master_data.employee', + ], + '/daily-checklist/master-data/activity/': [ + 'lti.daily_checklist.master_data.activity', + ], + '/daily-checklist/master-data/configuration/': [ + 'lti.daily_checklist.master_data.configuration', + ], // Production // Production - Project Flock From bf38178969dc6ec919201c0c4da1637894f920dc Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 11:46:19 +0700 Subject: [PATCH 39/49] chore: update product type --- src/types/api/master-data/product.d.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/types/api/master-data/product.d.ts b/src/types/api/master-data/product.d.ts index e82f857e..7fd2c7c1 100644 --- a/src/types/api/master-data/product.d.ts +++ b/src/types/api/master-data/product.d.ts @@ -1,20 +1,20 @@ import { BaseMetadata } from '@/types/api/api-general'; import { Uom } from '@/types/api/master-data/uom'; import { ProductCategory } from '@/types/api/master-data/product-category'; -import { Supplier } from '@/types/api/master-data/supplier'; +import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier'; export type BaseProduct = { id: number; name: string; brand: string; - sku: string; + sku?: string; product_price: number; selling_price?: number; tax?: number; - expiry_period: number; + expiry_period?: number; uom: Uom; product_category: ProductCategory; - suppliers: Supplier[]; + suppliers: (BaseSupplier & { price: number })[]; flags: string[]; }; @@ -23,14 +23,17 @@ export type Product = BaseMetadata & BaseProduct; export type CreateProductPayload = { name: string; brand: string; - sku: string; + sku?: string; uom_id: number; product_category_id: number; product_price: number; - selling_price: number; - tax: number; - expiry_period: number; - supplier_ids: number[]; + selling_price?: number; + tax?: number; + expiry_period?: number; + suppliers: { + supplier_id: number; + price: number; + }[]; flags: string[]; }; From 6a7990e722789afde820c49475a8368a4b3662c7 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 11:59:25 +0700 Subject: [PATCH 40/49] fix: make suppliers optional --- .../product/form/ProductForm.schema.ts | 1 - .../master-data/product/form/ProductForm.tsx | 23 +++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts index b85d5d4c..8a1d3de2 100644 --- a/src/components/pages/master-data/product/form/ProductForm.schema.ts +++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts @@ -92,7 +92,6 @@ export const ProductFormSchema: Yup.ObjectSchema = .typeError('Harga wajib diisi!'), }) ) - .min(1, 'Minimal harus ada 1 supplier!') .required('Supplier wajib diisi!'), flags: Yup.array() diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 7e893f67..8c04d594 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -527,6 +527,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { idx )} isClearable + isDisabled={type === 'detail'} className={{ wrapper: 'min-w-48 w-full', }} @@ -573,16 +574,18 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
-
- -
+ {type !== 'detail' && ( +
+ +
+ )} )}
From 781a5ca0d975b0d5969403fddfe7261247f5d971 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 15 Jan 2026 15:14:04 +0700 Subject: [PATCH 41/49] 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 42/49] 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 43/49] 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 44/49] 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 45/49] 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 46/49] 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' > - +
} + 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 = ({