From f08fae4f77de45eace467609b1577ba62fd5d4af Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 31 Dec 2025 13:00:48 +0700 Subject: [PATCH 01/32] refactor(FE): Enable reinitialize and map expedition vendor --- .../form/order/PurchaseOrderAcceptApprovalForm.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index d6ef5952..85d5a6e2 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -164,6 +164,7 @@ const PurchaseOrderAcceptApprovalForm = ({ validationSchema: PurchaseRequestAcceptApprovalFormSchema, validateOnChange: true, validateOnBlur: true, + enableReinitialize: true, onSubmit: async (values) => { const payload: CreateAcceptApprovalRequestPayload = { action: 'APPROVED', @@ -238,7 +239,12 @@ const PurchaseOrderAcceptApprovalForm = ({ travel_number: item.travel_number || '', travel_document_path: item.travel_document_path || '', vehicle_number: item.vehicle_number || '', - expedition_vendor: null, + expedition_vendor: item.expedition_vendor + ? { + value: item.expedition_vendor.id, + label: item.expedition_vendor.name, + } + : null, expedition_vendor_id: item.expedition_vendor_id || 0, received_qty: item.total_qty || '', transport_per_item: item.transport_per_item || '', From a5188950962fe30663c29490ff00e36168a42cc3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 2 Jan 2026 08:47:08 +0700 Subject: [PATCH 02/32] refactor(FE): Conditionally render sampling and result sections --- .../detail/UniformityDetailsPreview.tsx | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx index d98bba23..21be03d7 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetailsPreview.tsx @@ -229,49 +229,52 @@ const UniformityDetailsPreview = ({ {/* Form Section */}
- {uniformity_details && uniformity_details.length > 0 ? ( + {info_umum || sampling || result ? (
{/* Sampling and Range */} -
-

Sampling and Range

- - data={samplingTableData} - columns={columnsSampling} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> -
- {/* Result */} -
-

Result

- - data={resultTableData} - columns={resultColumns} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> -
+ {sampling && ( +
+

Sampling and Range

+ + data={samplingTableData} + columns={columnsSampling} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ )} - {/* Body Weight Details Button */} -
- -
- {/*{!uniformity_details || uniformity_details.length === 0 ? ( - <> - ) : null}*/} + {/* Result */} + {result && ( +
+

Result

+ + data={resultTableData} + columns={resultColumns} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ )} + + {!uniformity_details || uniformity_details.length === 0 ? ( +
+ +
+ ) : null} {/* Body Weight Details */} {uniformity_details && uniformity_details.length > 0 && ( From b1f4b4dc4b2e821a55d170a89e10638bcfac6944 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 2 Jan 2026 09:03:42 +0700 Subject: [PATCH 03/32] refactor(FE): Refresh uniformities on successful mutation --- .../pages/production/uniformity/UniformityTable.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index cf981732..c3851b85 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -355,6 +355,12 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { mutate: refreshUniformities, } = useSWR(uniformitySwrKey, UniformityApi.getAllFetcher); + useEffect(() => { + if (isSuccess) { + refreshUniformities(); + } + }, [isSuccess, refreshUniformities]); + // ===== FILTER HANDLERS ===== const handleFilterLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { From 6a3d2c0dcd1fd9668ebc660b9c26b9cdefdebc6a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 2 Jan 2026 09:07:30 +0700 Subject: [PATCH 04/32] refactor(FE): Drop refresh prop and simplify UniformityTable --- .../uniformity/UniformityPageWrapper.tsx | 4 +-- .../production/uniformity/UniformityTable.tsx | 26 +++++-------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityPageWrapper.tsx b/src/components/pages/production/uniformity/UniformityPageWrapper.tsx index bbc82665..ac14ebb5 100644 --- a/src/components/pages/production/uniformity/UniformityPageWrapper.tsx +++ b/src/components/pages/production/uniformity/UniformityPageWrapper.tsx @@ -41,9 +41,7 @@ export default function UniformityPageWrapper({ return ( <>
- !isOpen && router.push('/production/uniformity')} - /> +
{ - // Uniformity data is never locked - checkbox is always enabled - return false; -}; - -const canApproveRejectUniformity = (uniformity: Uniformity): boolean => { - return uniformity.status === 'CREATED' || uniformity.status === 'Pengajuan'; -}; - -interface UniformityPreviewData { - id: string; - label: string; - value: string; -} - const UniformityConfirmationPreview = ({ uniformity, }: { uniformity?: Uniformity; }) => { - const data: UniformityPreviewData[] = [ + const data: DetailOptionType[] = [ { id: 'tanggal', label: 'Tanggal', @@ -100,7 +88,7 @@ const UniformityConfirmationPreview = ({ }, ]; - const columns: ColumnDef[] = [ + const columns: ColumnDef[] = [ { accessorKey: 'label', header: 'Label', @@ -148,7 +136,7 @@ const UniformityConfirmationPreview = ({ ); }; -const UniformityTable = ({ refresh }: { refresh?: () => void }) => { +const UniformityTable = () => { const router = useRouter(); const searchParams = useSearchParams(); const isSuccess = useUniformityStore((s) => s.isSuccess); From 1c77deeee7eb1be0ed6b1cfbebf06ab94779314d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 2 Jan 2026 10:00:14 +0700 Subject: [PATCH 05/32] refactor(FE): Move Movement FormData into API service --- .../inventory/movement/form/MovementForm.tsx | 35 ++++++++----------- src/services/api/inventory.ts | 32 +++++++++++++++-- src/types/api/inventory/movement.d.ts | 7 +++- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index ae9b68d9..a4c24330 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -55,16 +55,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { // ===== FORM HANDLERS ===== const createMovementHandler = useCallback( - async (payload: CreateMovementPayload, documents: File[] = []) => { - const formData = new FormData(); - formData.append('data', JSON.stringify(payload)); - documents.forEach((file, index) => { - formData.append(`documents[${index}]`, file); - }); - - const res = await MovementApi.create( - formData as unknown as CreateMovementPayload - ); + async (payload: CreateMovementPayload) => { + const res = await MovementApi.createMovement(payload); if (isResponseError(res)) { setMovementFormErrorMessage(res.message); return; @@ -218,20 +210,23 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }); const payload: CreateMovementPayload = { - transfer_reason: values.transfer_reason, - transfer_date: values.transfer_date, - source_warehouse_id: values.source_warehouse_id, - destination_warehouse_id: values.destination_warehouse_id, - products: values.products.map((p) => ({ - product_id: p.product_id, - product_qty: parseInt(p.product_qty.toString()) || 0, - })), - deliveries: deliveriesPayload, + data: { + transfer_reason: values.transfer_reason, + transfer_date: values.transfer_date, + source_warehouse_id: values.source_warehouse_id, + destination_warehouse_id: values.destination_warehouse_id, + products: values.products.map((p) => ({ + product_id: p.product_id, + product_qty: parseInt(p.product_qty.toString()) || 0, + })), + deliveries: deliveriesPayload, + }, + documents: documents.length > 0 ? documents : undefined, }; switch (type) { case 'add': - await createMovementHandler(payload, documents); + await createMovementHandler(payload); break; } }, diff --git a/src/services/api/inventory.ts b/src/services/api/inventory.ts index fa406917..70a7c8f9 100644 --- a/src/services/api/inventory.ts +++ b/src/services/api/inventory.ts @@ -1,4 +1,5 @@ import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; import { CreateProductWarehousePayload, ProductWarehouse, @@ -20,11 +21,38 @@ export const ProductWarehouseApi = new BaseApiService< UpdateProductWarehousePayload >('/inventory/product-warehouses'); -export const MovementApi = new BaseApiService< +export class MovementApiService extends BaseApiService< Movement, CreateMovementPayload, unknown ->('/inventory/transfers'); +> { + constructor(basePath: string) { + super(basePath); + } + + async createMovement( + payload: CreateMovementPayload + ): Promise | undefined> { + const formData = new FormData(); + + // Append data as JSON string + formData.append('data', JSON.stringify(payload.data)); + + // Append documents if any + if (payload.documents && payload.documents.length > 0) { + payload.documents.forEach((file) => { + formData.append('documents', file); + }); + } + + return await this.customRequest>('', { + method: 'POST', + payload: formData as unknown as Record, + }); + } +} + +export const MovementApi = new MovementApiService('/inventory/transfers'); export const InventoryAdjustmentApi = new BaseApiService< InventoryAdjustment, diff --git a/src/types/api/inventory/movement.d.ts b/src/types/api/inventory/movement.d.ts index 53dfa61d..3114518f 100644 --- a/src/types/api/inventory/movement.d.ts +++ b/src/types/api/inventory/movement.d.ts @@ -49,7 +49,7 @@ export type BaseMovement = { export type Movement = BaseMetadata & BaseMovement; -export type CreateMovementPayload = { +export type CreateMovementPayloadData = { transfer_reason: string; transfer_date: string; source_warehouse_id: number; @@ -71,3 +71,8 @@ export type CreateMovementPayload = { }[]; }[]; }; + +export type CreateMovementPayload = { + data: CreateMovementPayloadData; + documents?: File[]; +}; From 57fa67c05a5821f9b43127469831a2d06e08a99a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 2 Jan 2026 14:22:57 +0700 Subject: [PATCH 06/32] refactor(FE): Support MovementDocument in movement form --- .../movement/form/MovementForm.schema.ts | 13 ++-- .../inventory/movement/form/MovementForm.tsx | 67 +++++++++++++------ src/types/api/inventory/movement.d.ts | 9 +++ 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.schema.ts b/src/components/pages/inventory/movement/form/MovementForm.schema.ts index 5ea844ab..9b2c8557 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.schema.ts +++ b/src/components/pages/inventory/movement/form/MovementForm.schema.ts @@ -1,5 +1,5 @@ import * as Yup from 'yup'; -import { Movement } from '@/types/api/inventory/movement'; +import { Movement, MovementDocument } from '@/types/api/inventory/movement'; type MovementFormSchemaType = { transfer_reason: string; @@ -29,7 +29,7 @@ type MovementFormSchemaType = { deliveries: { delivery_cost?: number | string; delivery_cost_per_item?: number | string; - document?: File | string | null; + document?: File | MovementDocument | null; document_path?: string | null; driver_name: string; vehicle_plate: string; @@ -61,7 +61,7 @@ export type ProductSchema = { export type DeliverySchema = { delivery_cost?: number | string; delivery_cost_per_item?: number | string; - document?: File | string | null; + document?: File | MovementDocument | null; document_path?: string | null; driver_name: string; vehicle_plate: string; @@ -129,13 +129,12 @@ const DeliveryObjectSchema: Yup.ObjectSchema = Yup.object({ }), document_path: Yup.string().optional(), document_index: Yup.number().optional(), - document: Yup.mixed() + document: Yup.mixed() .nullable() .test('fileSize', 'Ukuran dokumen maksimal 2 MB', (value) => { if (!value) return true; - if (typeof value === 'string') return true; if (value instanceof File) return value.size <= 2 * 1024 * 1024; - return false; + return true; }), driver_name: Yup.string().required('Nama sopir wajib diisi!'), vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'), @@ -241,7 +240,7 @@ export const getMovementFormInitialValues = ( delivery_cost: d.shipping_cost_total ?? undefined, delivery_cost_per_item: d.shipping_cost_item ?? undefined, document_number: d.document_number ?? '', - document: d.document_path ?? null, + document: d.document ?? null, document_path: d.document_path ?? null, driver_name: d.driver_name ?? '', vehicle_plate: d.vehicle_plate ?? '', diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index a4c24330..3c49295e 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -35,6 +35,7 @@ import FileInput from '@/components/input/FileInput'; import CheckboxInput from '@/components/input/CheckboxInput'; import Badge from '@/components/Badge'; import Card from '@/components/Card'; +import { S3_PUBLIC_BASE_URL } from '@/config/constant'; interface MovementFormProps { type?: 'add' | 'edit' | 'detail'; @@ -1532,27 +1533,51 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { {type === 'detail' ? ( <>
- + {delivery.document_path ? ( + + ) : delivery.document && + delivery.document instanceof File === false ? ( + + ) : ( + + )}
) : ( diff --git a/src/types/api/inventory/movement.d.ts b/src/types/api/inventory/movement.d.ts index 3114518f..2f6caceb 100644 --- a/src/types/api/inventory/movement.d.ts +++ b/src/types/api/inventory/movement.d.ts @@ -14,6 +14,14 @@ type MovementWarehouse = { }; }; +export type MovementDocument = { + id: number; + path: string; + name: string; + ext: string; + size: number; +}; + export type BaseMovement = { id: number; transfer_reason: string; @@ -39,6 +47,7 @@ export type BaseMovement = { document_path: string; shipping_cost_item: number; shipping_cost_total: number; + document?: MovementDocument; items: { id: number; stock_transfer_detail_id: number; From 7579cd55332d25b61d6585b26e5daa47c4bfe424 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 5 Jan 2026 08:56:09 +0700 Subject: [PATCH 07/32] refactor(FE): Remove Kandang prop and center Uniformity gauge --- .../production/uniformity/UniformityChart.tsx | 3 --- .../uniformity/chart/UniformityGaugeChart.tsx | 20 ++++++------------- .../skeleton/UniformityGaugeChartSkeleton.tsx | 2 +- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 1b58b16c..233d9d67 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -72,7 +72,6 @@ const UniformityChart = () => { // const gaugeChartData: GaugeChartData = { // value: 0, // label: '', - // kandang: 'Kandang Cirangga', // week: 'Week 2', // currentValue: 512, // totalValue: 1024, @@ -81,7 +80,6 @@ const UniformityChart = () => { const gaugeChartData: GaugeChartData = { value: 52, label: 'Uniformity', - kandang: 'Kandang Cirangga', week: 'Week 2', currentValue: 512, totalValue: 1024, @@ -128,7 +126,6 @@ const UniformityChart = () => { = ({ value, label, - kandang, week, currentValue, totalValue, @@ -34,7 +31,7 @@ const UniformityGaugeChart: React.FC = ({ const inactiveColor = '#f0f0f0'; return ( -
+
@@ -76,18 +73,13 @@ const UniformityGaugeChart: React.FC = ({ -
-
- -
-
-
- {kandang} - - +
+
+
+ {week}
diff --git a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx index 02ec70c1..436fab9a 100644 --- a/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx +++ b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx @@ -28,7 +28,7 @@ const UniformityGaugeChartSkeleton: React.FC< const inactiveColor = '#f0f0f0'; return ( -
+
From 62b05bf9c0cb699d76f781b056f870168664afb0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 5 Jan 2026 09:11:54 +0700 Subject: [PATCH 08/32] refactor(FE): Add week navigation to UniformityGaugeChart --- .../uniformity/chart/UniformityGaugeChart.tsx | 73 +++++++++++++------ 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx index e8d2a5ca..04fbef9c 100644 --- a/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; import Card from '@/components/Card'; import { formatNumber } from '@/lib/helper'; +import { Icon } from '@iconify/react'; interface UniformityGaugeChartProps { value: number; @@ -9,6 +10,9 @@ interface UniformityGaugeChartProps { week?: string; currentValue?: number; totalValue?: number; + onWeekChange?: (direction: 'prev' | 'next') => void; + hasPrevWeek?: boolean; + hasNextWeek?: boolean; } const UniformityGaugeChart: React.FC = ({ @@ -17,6 +21,9 @@ const UniformityGaugeChart: React.FC = ({ week, currentValue, totalValue, + onWeekChange, + hasPrevWeek = false, + hasNextWeek = false, }) => { const numberOfSegments = 50; const filledSegments = Math.round((value / 100) * numberOfSegments); @@ -70,29 +77,49 @@ const UniformityGaugeChart: React.FC = ({
- -
-
-
- - {week} - +
+ + +
+
+
+ + {week} + +
+
+ + {formatNumber(currentValue ?? 0)} + + From + + {formatNumber(totalValue ?? 0)} + +
-
- - {formatNumber(currentValue ?? 0)} - - From - {formatNumber(totalValue ?? 0)} -
-
-
-
+
+ + +
); }; From d7ef86e24be11358a9d60f4d02628aeb6c2d6753 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 5 Jan 2026 13:46:21 +0700 Subject: [PATCH 09/32] refactor(FE): Add default page/limit to location and flock queries --- .../pages/production/uniformity/form/UniformityForm.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/uniformity/form/UniformityForm.tsx b/src/components/pages/production/uniformity/form/UniformityForm.tsx index 2daac10d..bbca72f8 100644 --- a/src/components/pages/production/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityForm.tsx @@ -97,12 +97,16 @@ const UniformityForm = ({ setInputValue: setLocationSelectInputValue, options: locationOptions, isLoadingOptions: isLoadingLocations, - } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + } = useSelect(LocationApi.basePath, 'id', 'name', 'search', { + page: '1', + limit: '100', + }); // ===== FETCH PROJECT FLOCKS DATA ===== const projectFlocksUrl = useMemo(() => { const params = new URLSearchParams({ search: projectFlockSearchValue || '', + page: '1', limit: '100', }); if (selectedLocation) { @@ -141,6 +145,7 @@ const UniformityForm = ({ const approvedProjectFlockKandangsUrl = useMemo(() => { const params = new URLSearchParams({ step_name: 'Disetujui', + page: '1', limit: '100', }); return `${ProjectFlockKandangApi.basePath}?${params.toString()}`; From 2c29cffa456bcd5d817f08312e795fb3768ef247 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 5 Jan 2026 14:50:10 +0700 Subject: [PATCH 10/32] refactor(FE): Remove overflow-visible class from table wrapper --- src/components/pages/production/recording/RecordingTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index e5bd30cb..39b17ef7 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -872,7 +872,7 @@ const RecordingTable = () => { 'mb-20': isResponseSuccess(recordings) && recordings?.data?.length === 0, }), - tableWrapperClassName: 'overflow-x-auto min-h-full overflow-visible!', + tableWrapperClassName: 'overflow-x-auto min-h-full!', tableClassName: 'font-inter w-full table-auto min-h-full!', headerRowClassName: 'border-b border-b-gray-200', headerColumnClassName: From d2b19cbd7bdd1edba7c452c8a0ed488f8d73ddba Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 5 Jan 2026 15:07:09 +0700 Subject: [PATCH 11/32] refactor(FE): Force form remount on initialValues change --- .../order/PurchaseOrderAcceptApprovalForm.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx index 85d5a6e2..b90a7c91 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm.tsx @@ -52,9 +52,14 @@ const PurchaseOrderAcceptApprovalForm = ({ const searchParams = useSearchParams(); const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] = useState(''); + const [key, setKey] = useState(0); const isRejected = initialValues?.latest_approval?.action === 'REJECTED'; + useEffect(() => { + setKey((prev) => prev + 1); + }, [initialValues?.id]); + // ===== UTILITY FUNCTIONS ===== const isRepeaterInputError = ( idx: number, @@ -164,7 +169,7 @@ const PurchaseOrderAcceptApprovalForm = ({ validationSchema: PurchaseRequestAcceptApprovalFormSchema, validateOnChange: true, validateOnBlur: true, - enableReinitialize: true, + enableReinitialize: false, onSubmit: async (values) => { const payload: CreateAcceptApprovalRequestPayload = { action: 'APPROVED', @@ -252,7 +257,7 @@ const PurchaseOrderAcceptApprovalForm = ({ }); formik.setFieldValue('items', updatedItems); } - }, [purchaseItems, initialValues]); + }, [purchaseItems, initialValues, key]); useEffect(() => { if ( @@ -342,7 +347,11 @@ const PurchaseOrderAcceptApprovalForm = ({ }; return ( -
+

{type === 'add' @@ -705,7 +714,9 @@ const PurchaseOrderAcceptApprovalForm = ({ color='warning' className='px-4' onClick={() => { - formik.resetForm(); + if (type === 'add') { + formik.resetForm(); + } setPurchaseOrderFormErrorMessage(''); onCancel?.(); onModalClose?.(); From fd32b55ad90db7cb1d9875caa719ca460bf6547c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 5 Jan 2026 15:13:00 +0700 Subject: [PATCH 12/32] refactor(FE): Provide defaults for missing info_umum fields --- .../uniformity/detail/UniformityDetail.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx index 0cc39d9a..4d0cd887 100644 --- a/src/components/pages/production/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx @@ -119,11 +119,13 @@ const UniformityDetail: React.FC = ({ const statusValue = latest_approval?.action ?? '-'; const valueMap: Record = { - tanggal: formatDate(info_umum.tanggal, 'DD MMMM YYYY'), - 'lokasi-farm': info_umum.lokasi_farm, - 'project-flock': info_umum.project_flock, - kandang: info_umum.kandang, - 'document-name': info_umum.file_name, + tanggal: info_umum?.tanggal + ? formatDate(info_umum.tanggal, 'DD MMMM YYYY') + : '-', + 'lokasi-farm': info_umum?.lokasi_farm ?? '-', + 'project-flock': info_umum?.project_flock ?? '-', + kandang: info_umum?.kandang ?? '-', + 'document-name': info_umum?.file_name ?? '-', 'approval-status': statusValue, }; From c24aebe02d5b355c85401f6dc77dca9ec7e50121 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 5 Jan 2026 15:29:52 +0700 Subject: [PATCH 13/32] refactor(FE): Highlight ideal ranges in Uniformity charts --- .../production/uniformity/UniformityChart.tsx | 78 +++++++++++++++---- .../uniformity/chart/UniformityBarChart.tsx | 16 +++- 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 233d9d67..58a8f9c8 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -1,13 +1,15 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import Card from '@/components/Card'; import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart'; import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart'; import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; +import { UniformityDetailItem } from '@/types/api/production/uniformity'; interface BarChartData { name: string; uv: number; + isIdeal?: boolean; } interface GaugeChartData { @@ -19,9 +21,18 @@ interface GaugeChartData { totalValue?: number; } -const UniformityChart = () => { - // TODO: Replace with actual API call - const barChartData: BarChartData[] = [ +interface UniformityChartProps { + barChartData?: BarChartData[]; + gaugeChartData?: GaugeChartData; + uniformityDetails?: UniformityDetailItem[]; +} + +const UniformityChart = ({ + barChartData: initialBarChartData, + gaugeChartData: initialGaugeChartData, + uniformityDetails, +}: UniformityChartProps) => { + const defaultBarChartData: BarChartData[] = [ { name: '48-52', uv: 80, @@ -68,16 +79,7 @@ const UniformityChart = () => { }, ]; - // TODO: Replace with actual API call - // const gaugeChartData: GaugeChartData = { - // value: 0, - // label: '', - // week: 'Week 2', - // currentValue: 512, - // totalValue: 1024, - // }; - - const gaugeChartData: GaugeChartData = { + const defaultGaugeChartData: GaugeChartData = { value: 52, label: 'Uniformity', week: 'Week 2', @@ -85,6 +87,48 @@ const UniformityChart = () => { totalValue: 1024, }; + const defaultUniformityDetails: UniformityDetailItem[] = [ + { id: 1, weight: 61, range: 'Ideal' }, + { id: 2, weight: 62, range: 'Ideal' }, + { id: 3, weight: 63, range: 'Ideal' }, + { id: 4, weight: 64, range: 'Ideal' }, + { id: 5, weight: 65, range: 'Ideal' }, + { id: 6, weight: 66, range: 'Ideal' }, + { id: 7, weight: 67, range: 'Ideal' }, + ]; + + const detailsToUse = uniformityDetails || defaultUniformityDetails; + + const barChartData = useMemo(() => { + const dataToProcess = initialBarChartData || defaultBarChartData; + + if (!detailsToUse || detailsToUse.length === 0) { + return dataToProcess; + } + + return dataToProcess.map((bar) => { + const rangeMatch = bar.name.match(/(\d+)-(\d+)/); + if (!rangeMatch) return bar; + + const minWeight = parseInt(rangeMatch[1], 10); + const maxWeight = parseInt(rangeMatch[2], 10); + + const hasIdealWeight = detailsToUse.some((detail) => { + const weight = detail.weight; + return ( + detail.range === 'Ideal' && weight >= minWeight && weight <= maxWeight + ); + }); + + return { + ...bar, + isIdeal: hasIdealWeight, + }; + }); + }, [initialBarChartData, detailsToUse]); + + const gaugeChartData = initialGaugeChartData || defaultGaugeChartData; + return (
{ )}

- {gaugeChartData.value === 0 ? ( + {gaugeChartData && gaugeChartData.value === 0 ? ( { > - ) : ( + ) : gaugeChartData ? ( { totalValue={gaugeChartData.totalValue} /> - )} + ) : null}
); }; diff --git a/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx b/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx index 5960a006..34dc134e 100644 --- a/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx @@ -3,6 +3,7 @@ import { Bar, BarChart, CartesianGrid, + Cell, Rectangle, ResponsiveContainer, Tooltip, @@ -25,6 +26,7 @@ interface CustomTooltipProps { interface BarChartData { name: string; uv: number; + isIdeal?: boolean; } interface UniformityBarChartProps { @@ -105,9 +107,6 @@ const UniformityBarChart: React.FC = ({ data }) => { = ({ data }) => { radius={[25, 25, 0, 0]} /> } - /> + > + {data.map((entry, index) => ( + + ))} +
); From 24499d110aaf5f6a89ccac9db70ce6bbbfe8cef2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 6 Jan 2026 08:36:23 +0700 Subject: [PATCH 14/32] refactor(FE): Refactor Uniformity charts to use API data --- .../production/uniformity/UniformityChart.tsx | 156 +++++++----------- .../production/uniformity/UniformityTable.tsx | 50 +++++- .../uniformity/chart/UniformityBarChart.tsx | 19 +++ 3 files changed, 125 insertions(+), 100 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx index 58a8f9c8..77d1608d 100644 --- a/src/components/pages/production/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -4,89 +4,20 @@ import UniformityBarChart from '@/components/pages/production/uniformity/chart/U import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart'; import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; -import { UniformityDetailItem } from '@/types/api/production/uniformity'; - -interface BarChartData { - name: string; - uv: number; - isIdeal?: boolean; -} - -interface GaugeChartData { - value: number; - label: string; - kandang?: string; - week?: string; - currentValue?: number; - totalValue?: number; -} +import { + UniformityDetailItem, + Uniformity, +} from '@/types/api/production/uniformity'; interface UniformityChartProps { - barChartData?: BarChartData[]; - gaugeChartData?: GaugeChartData; + uniformityData?: Uniformity | null; uniformityDetails?: UniformityDetailItem[]; } const UniformityChart = ({ - barChartData: initialBarChartData, - gaugeChartData: initialGaugeChartData, + uniformityData, uniformityDetails, }: UniformityChartProps) => { - const defaultBarChartData: BarChartData[] = [ - { - name: '48-52', - uv: 80, - }, - { - name: '52-56', - uv: 120, - }, - { - name: '56-60', - uv: 160, - }, - { - name: '60-64', - uv: 200, - }, - { - name: '64-68', - uv: 160, - }, - { - name: '68-72', - uv: 120, - }, - { - name: '72-76', - uv: 80, - }, - { - name: '76-80', - uv: 120, - }, - { - name: '84-88', - uv: 160, - }, - { - name: '88-92', - uv: 200, - }, - { - name: '92-96', - uv: 160, - }, - ]; - - const defaultGaugeChartData: GaugeChartData = { - value: 52, - label: 'Uniformity', - week: 'Week 2', - currentValue: 512, - totalValue: 1024, - }; - const defaultUniformityDetails: UniformityDetailItem[] = [ { id: 1, weight: 61, range: 'Ideal' }, { id: 2, weight: 62, range: 'Ideal' }, @@ -100,34 +31,63 @@ const UniformityChart = ({ const detailsToUse = uniformityDetails || defaultUniformityDetails; const barChartData = useMemo(() => { - const dataToProcess = initialBarChartData || defaultBarChartData; - - if (!detailsToUse || detailsToUse.length === 0) { - return dataToProcess; + if (!uniformityData) { + return []; } - return dataToProcess.map((bar) => { - const rangeMatch = bar.name.match(/(\d+)-(\d+)/); - if (!rangeMatch) return bar; + if (!detailsToUse || detailsToUse.length === 0) { + return []; + } - const minWeight = parseInt(rangeMatch[1], 10); - const maxWeight = parseInt(rangeMatch[2], 10); + const weights = detailsToUse.map((d) => d.weight); + const minWeight = Math.floor(Math.min(...weights) / 5) * 5; + const maxWeight = Math.ceil(Math.max(...weights) / 5) * 5; - const hasIdealWeight = detailsToUse.some((detail) => { - const weight = detail.weight; - return ( - detail.range === 'Ideal' && weight >= minWeight && weight <= maxWeight - ); - }); + const rangeSize = maxWeight - minWeight < 11 ? 4 : 5; + const ranges: string[] = []; + + for (let start = minWeight; start <= maxWeight; start += rangeSize) { + const end = start + rangeSize; + ranges.push(`${start}-${end}`); + } + + const totalIdealCount = detailsToUse.filter( + (d) => d.range === 'Ideal' + ).length; + + return ranges.map((range) => { + const [minStr, maxStr] = range.split('-').map(Number); + const min = minStr; + const max = maxStr; + + const birdsInRange = detailsToUse.filter( + (d) => d.weight >= min && d.weight < max + ).length; + + const hasIdeal = detailsToUse.some( + (d) => d.range === 'Ideal' && d.weight >= min && d.weight < max + ); return { - ...bar, - isIdeal: hasIdealWeight, + name: range, + uv: birdsInRange, + isIdeal: hasIdeal, + idealCount: hasIdeal ? totalIdealCount : undefined, }; }); - }, [initialBarChartData, detailsToUse]); + }, [uniformityData, detailsToUse]); - const gaugeChartData = initialGaugeChartData || defaultGaugeChartData; + const gaugeChartData = useMemo(() => { + if (!uniformityData) return undefined; + + return { + value: uniformityData.uniformity, + label: 'Uniformity', + week: `Week ${uniformityData.week}`, + currentValue: uniformityData.uniform_qty, + totalValue: uniformityData.chick_qty_of_weight, + }; + }, [uniformityData]); return (
@@ -140,14 +100,14 @@ const UniformityChart = ({ }} >
- {barChartData.length === 0 ? ( + {!uniformityData || barChartData.length === 0 ? ( ) : ( )}
- {gaugeChartData && gaugeChartData.value === 0 ? ( + {!uniformityData || !gaugeChartData ? ( - ) : gaugeChartData ? ( + ) : ( - ) : null} + )}
); }; diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 0a7483f2..e031e2c4 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -13,6 +13,7 @@ import { UniformityApi } from '@/services/api/uniformity'; import { DetailOptionType, type Uniformity, + type UniformityDetail, } from '@/types/api/production/uniformity'; import { isResponseSuccess } from '@/lib/api-helper'; import { type BaseApiResponse } from '@/types/api/api-general'; @@ -79,7 +80,7 @@ const UniformityConfirmationPreview = ({ { id: 'file-uniformity', label: 'File Uniformity', - value: '-', // File name tidak tersedia di GET ALL response + value: '-', }, { id: 'status', @@ -136,6 +137,51 @@ const UniformityConfirmationPreview = ({ ); }; +const UniformityChartWrapper = ({ + uniformitySwrKey, +}: { + uniformitySwrKey: string; +}) => { + const { data: uniformities } = useSWR( + uniformitySwrKey, + UniformityApi.getAllFetcher + ); + + const uniformityData = useMemo(() => { + if (isResponseSuccess(uniformities) && uniformities?.data?.length > 0) { + return uniformities.data[0]; + } + return null; + }, [uniformities]); + + const shouldFetchDetails = !!uniformityData; + const uniformityDetailSwrKey = useMemo(() => { + if (!uniformityData) return null; + return `${UniformityApi.basePath}/${uniformityData.id}?with_details=true`; + }, [uniformityData]); + + const { data: uniformityDetailResponse } = useSWR( + uniformityDetailSwrKey, + shouldFetchDetails ? UniformityApi.getAllFetcher : null + ); + + const uniformityDetails = useMemo(() => { + if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) { + const detailData = + uniformityDetailResponse.data as unknown as UniformityDetail; + return detailData.uniformity_details; + } + return undefined; + }, [shouldFetchDetails, uniformityDetailResponse]); + + return ( + + ); +}; + const UniformityTable = () => { const router = useRouter(); const searchParams = useSearchParams(); @@ -830,7 +876,7 @@ const UniformityTable = () => {
- +
+

Uniformity 2025

+
+
+
+ {chartData.idealCount} of Birds +
+ {labelStr} +
+
+ ); + } + return (

Uniformity 2025

From a4275f4b663826b350f59186e1d0463a5abdd521 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 6 Jan 2026 10:46:05 +0700 Subject: [PATCH 15/32] refactor(FE): Support UniformityDetail in confirmation preview --- .../production/uniformity/UniformityTable.tsx | 54 +++++++------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index e031e2c4..3df04a97 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -51,41 +51,54 @@ import MenuItem from '@/components/menu/MenuItem'; const UniformityConfirmationPreview = ({ uniformity, + uniformityDetail, }: { uniformity?: Uniformity; + uniformityDetail?: UniformityDetail; }) => { + const fileName = uniformityDetail?.info_umum?.file_name || '-'; + const data: DetailOptionType[] = [ { id: 'tanggal', label: 'Tanggal', value: uniformity ? formatDate(uniformity.applied_at, 'DD MMM YYYY') - : '-', + : uniformityDetail + ? formatDate(uniformityDetail.info_umum.tanggal, 'DD MMM YYYY') + : '-', }, { id: 'lokasi-farm', label: 'Lokasi Farm', - value: uniformity?.location_name || '-', + value: + uniformity?.location_name || + uniformityDetail?.info_umum?.lokasi_farm || + '-', }, { id: 'project-flock', label: 'Project Flock', - value: uniformity?.flock_name || '-', + value: + uniformity?.flock_name || + uniformityDetail?.info_umum?.project_flock || + '-', }, { id: 'kandang', label: 'Kandang', - value: uniformity?.kandang_name || '-', + value: + uniformity?.kandang_name || uniformityDetail?.info_umum?.kandang || '-', }, { id: 'file-uniformity', label: 'File Uniformity', - value: '-', + value: fileName, }, { id: 'status', label: 'Status', - value: uniformity?.status || '-', + value: uniformity?.status || (uniformityDetail ? 'CREATED' : '-'), }, ]; @@ -938,34 +951,7 @@ const UniformityTable = () => {
{createdUniformity ? ( ) : selectedRowIds.length === 1 ? ( Date: Tue, 6 Jan 2026 11:04:54 +0700 Subject: [PATCH 16/32] refactor(FE): Check delete API response before showing toast --- .../pages/expense/ExpenseRequestContent.tsx | 12 ++++++------ src/components/pages/expense/ExpensesTable.tsx | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index 2b9086e0..657c5e5c 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -140,17 +140,17 @@ const ExpenseRequestContent = ({ const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - try { - await ExpenseApi.delete(initialValues?.id as number); + const deleteResponse = await ExpenseApi.delete(initialValues?.id as number); + if (isResponseSuccess(deleteResponse)) { toast.success('Berhasil menghapus data biaya operasional!'); router.push('/expense'); - } catch (error) { + } else { toast.error('Gagal menghapus data biaya operasional!'); - } finally { - deleteModal.closeModal(); - setIsDeleteLoading(false); } + + deleteModal.closeModal(); + setIsDeleteLoading(false); }; const confirmationModalCompleteClickHandler = async () => { diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 9ae3ed34..1f3e9df5 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -420,11 +420,19 @@ const ExpensesTable = () => { const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); - await ExpenseApi.delete(selectedExpense?.id as number); - refreshExpenses(); + const deleteResponse = await ExpenseApi.delete( + selectedExpense?.id as number + ); + + if (isResponseSuccess(deleteResponse)) { + refreshExpenses(); + deleteModal.closeModal(); + toast.success('Berhasil menghapus biaya operasional!'); + } else { + deleteModal.closeModal(); + toast.error('Gagal menghapus biaya operasional!'); + } - deleteModal.closeModal(); - toast.success('Berhasil menghapus biaya operasional!'); setIsDeleteLoading(false); }; From 841aadc107d9f3675641f170fe9bc25418fc9b58 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Tue, 6 Jan 2026 13:29:05 +0700 Subject: [PATCH 17/32] fix(FE): fixing issue reject modal show up when creating project flock --- .../project-flock/ProjectFlockTable.tsx | 5 +- .../project-flock/form/ProjectFlockForm.tsx | 133 +----------------- 2 files changed, 5 insertions(+), 133 deletions(-) diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 7d9ce7da..f6888c3d 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -308,7 +308,10 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
)} - {approvals && !approvalsLoading && formType == 'detail' && ( - - )} - {formType == 'detail' && ( -
- - - - - - -
- )} +
- {/*
-
- {JSON.stringify(formik.values)} -
-
- {JSON.stringify(formik.errors)} -
-
*/} {formType !== 'detail' && ( - - { - confirmApprovalHandler(notes, approvalAction); - }, - }} - /> ); }; From f22c4e4798b45386fb2e77725dce400859db1181 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 6 Jan 2026 13:32:59 +0700 Subject: [PATCH 18/32] refactor(FE): Adjust expense status badge colors --- src/components/pages/expense/ExpenseStatusBadge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/expense/ExpenseStatusBadge.tsx b/src/components/pages/expense/ExpenseStatusBadge.tsx index 3a84f6bc..a70b6454 100644 --- a/src/components/pages/expense/ExpenseStatusBadge.tsx +++ b/src/components/pages/expense/ExpenseStatusBadge.tsx @@ -21,7 +21,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { switch (latestApprovalStepNumber) { case 1: - expenseStatusPillBadgeColor = 'yellow'; + expenseStatusPillBadgeColor = 'gray'; break; case 2: @@ -33,7 +33,7 @@ const ExpenseStatusBadge = ({ approval }: ExpenseStatusBadgeProps) => { break; case 4: - expenseStatusPillBadgeColor = 'red'; + expenseStatusPillBadgeColor = 'yellow'; break; case 5: From 0af612703a5066a0d7a8eaf96350b9d0d28a1955 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Tue, 6 Jan 2026 13:46:18 +0700 Subject: [PATCH 19/32] fix(FE): remove pullet table, change doc table to conditional data base on project flock category --- .../pages/closing/ClosingDetail.tsx | 7 ++- .../ClosingSapronakCalculationTabContent.tsx | 10 +++-- .../ClosingSapronakCalculationTable.tsx | 45 +++++++------------ 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index 94647f87..3de2ffe9 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -45,7 +45,12 @@ const ClosingDetail: React.FC = ({ { id: 'perhitunganSapronak', label: 'Perhitungan Sapronak', - content: , + content: ( + + ), }, { id: 'penjualan', diff --git a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx b/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx index 15e43bbc..b8add15b 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx @@ -1,21 +1,25 @@ 'use client'; -import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable'; -import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable'; import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable'; +import { ClosingGeneralInformation } from '@/types/api/closing'; interface ClosingSapronakCalculationTabContentProps { projectFlockId?: number; + closingGeneralInformation?: ClosingGeneralInformation; } const ClosingSapronakCalculationTabContent = ({ projectFlockId, + closingGeneralInformation, }: ClosingSapronakCalculationTabContentProps) => { return (
{projectFlockId && ( <> - + )}
diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx index 22b4d2e2..6e3b1a95 100644 --- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -13,15 +13,16 @@ import { useMemo } from 'react'; import useSWR from 'swr'; import { ClosingApi } from '@/services/api/closing'; import { isResponseSuccess } from '@/lib/api-helper'; +import { ClosingGeneralInformation } from '@/types/api/closing'; interface ClosingSapronakCalculationTableProps { - type?: 'detail'; projectFlockId: number; + closingGeneralInformation?: ClosingGeneralInformation; } const ClosingSapronakCalculationTable = ({ - type, projectFlockId, + closingGeneralInformation, }: ClosingSapronakCalculationTableProps) => { const { data: sapronakCalculation, isLoading } = useSWR( `/closing/sapronak-calculation/${projectFlockId}`, @@ -182,8 +183,13 @@ const ClosingSapronakCalculationTable = ({ return (
+ {/* Table DOC jika kategori Project Flock Growing */} data={ isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.doc?.rows ?? []) + ? ((closingGeneralInformation?.project_category === 'GROWING' + ? sapronakCalculation.data?.doc?.rows + : sapronakCalculation.data?.pullet?.rows) ?? []) : [] } - columns={docColumns} + columns={ + closingGeneralInformation?.project_category === 'GROWING' + ? docColumns + : pulletColumns + } className={{ containerClassName: 'my-4', }} @@ -250,29 +262,6 @@ const ClosingSapronakCalculationTable = ({ renderFooter={isResponseSuccess(sapronakCalculation)} /> - - - - data={ - isResponseSuccess(sapronakCalculation) - ? (sapronakCalculation.data?.pullet?.rows ?? []) - : [] - } - columns={pulletColumns} - className={{ - containerClassName: 'my-4', - }} - renderFooter={isResponseSuccess(sapronakCalculation)} - /> -
); }; From 2fa086bb3232692846e20679b67af2e6b10d224d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 6 Jan 2026 14:01:01 +0700 Subject: [PATCH 20/32] refactor(FE): Prefer latest_approval action and add file_name --- .../production/uniformity/UniformityTable.tsx | 21 ++++++++++++------- src/types/api/production/uniformity.d.ts | 4 +--- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 3df04a97..0c0c3f70 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -56,8 +56,6 @@ const UniformityConfirmationPreview = ({ uniformity?: Uniformity; uniformityDetail?: UniformityDetail; }) => { - const fileName = uniformityDetail?.info_umum?.file_name || '-'; - const data: DetailOptionType[] = [ { id: 'tanggal', @@ -93,7 +91,8 @@ const UniformityConfirmationPreview = ({ { id: 'file-uniformity', label: 'File Uniformity', - value: fileName, + value: + uniformity?.file_name || uniformityDetail?.info_umum?.file_name || '-', }, { id: 'status', @@ -461,9 +460,15 @@ const UniformityTable = () => { const canApproveReject = useMemo(() => { return ( selectedUniformities.length > 0 && - selectedUniformities.every( - (u) => u.status === 'CREATED' || u.status === 'Pengajuan' - ) + selectedUniformities.every((u) => { + const approvalAction = u.latest_approval?.action; + return ( + approvalAction === 'CREATED' || + approvalAction === 'Pengajuan' || + (!approvalAction && + (u.status === 'CREATED' || u.status === 'Pengajuan')) + ); + }) ); }, [selectedUniformities]); @@ -818,7 +823,9 @@ const UniformityTable = () => { accessorKey: 'status', header: 'Status', cell: (props) => { - const status = props.row.original.status; + const uniformity = props.row.original; + const status = + uniformity.latest_approval?.action ?? uniformity.status; return (
Date: Tue, 6 Jan 2026 14:42:52 +0700 Subject: [PATCH 21/32] feat(FE): adding stok information in form repeater SO and DO --- .../pages/marketing/MarketingTable.tsx | 3 +- .../delivery-order/DeliverOrderProduct.tsx | 17 +++++++++-- .../sales-order/SalesOrderProductForm.tsx | 30 +++++++++++++++++-- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 507819e3..1c37dbbb 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -682,7 +682,7 @@ const MarketingTable = () => { @@ -724,6 +724,7 @@ const MarketingTable = () => { }, ]} className={{ + containerClassName: 'p-6', tableWrapperClassName: 'overflow-x-auto min-h-full!', tableClassName: 'font-inter w-full table-auto min-h-full!', headerRowClassName: 'border-b border-b-gray-200', 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 a0eed811..5c81396e 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -15,6 +15,7 @@ import { BaseSalesOrder } from '@/types/api/marketing/marketing'; import Badge from '@/components/Badge'; import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm'; import * as Yup from 'yup'; +import { isResponseSuccess } from '@/lib/api-helper'; const DeliveryOrderProductForm = ({ formState, @@ -208,7 +209,7 @@ const DeliveryOrderProductForm = ({ ...formik.values, marketing_product_id: undefined, marketing_product: null, - qty: formik.values.qty || '', + qty: '', unit_price: '', total_price: '', avg_weight: '', @@ -222,7 +223,7 @@ const DeliveryOrderProductForm = ({ ...formik.values, marketing_product_id: selected.value as number, marketing_product: SalesProductToFieldValues(so), - qty: formik.values.qty || so.qty, + qty: so.qty, unit_price: so.unit_price, total_price: so.total_price, avg_weight: so.avg_weight, @@ -298,8 +299,18 @@ const DeliveryOrderProductForm = ({ isError={Boolean(formik.errors.qty)} errorMessage={formik.errors.qty} placeholder='Masukan Kuantitas' + bottomLabel={ + formik.values.marketing_product_id + ? 'Stok dijual: ' + + salesOrders?.find( + (item) => item.id === formik.values.marketing_product_id + )?.qty + : '' + } /> - +
+
+
(''); + // ============ Formik ============ const formik = useFormik({ enableReinitialize: true, initialValues: { @@ -58,6 +63,7 @@ const SalesOrderProductForm = ({ isInitialValid: false, }); + // ===== Options ===== const { options: kandangSourceOptions, isLoadingOptions: isLoadingKandangSourceOptions, @@ -86,12 +92,13 @@ const SalesOrderProductForm = ({ ); }, [warehouseSourceOptions, exisitingValues]); + // ===== Handler ===== const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { formik.setFieldValue('kandang', val as OptionType); formik.setFieldValue('kandang_id', (val as OptionType)?.value); formik.setFieldValue('product_warehouse_id', null); formik.setFieldValue('product_warehouse', null); - formik.setFieldValue('qty', null); + formik.setFieldValue('qty', ''); }; const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { @@ -106,7 +113,7 @@ const SalesOrderProductForm = ({ formik.setFieldValue('qty', productWarehouse?.quantity); handleBlurField('qty'); } else { - formik.setFieldValue('qty', null); + formik.setFieldValue('qty', ''); } }; @@ -248,7 +255,24 @@ const SalesOrderProductForm = ({ isError={formik.touched.qty && Boolean(formik.errors.qty)} errorMessage={formik.errors.qty} placeholder='Masukan Kuantitas' + bottomLabel={ + isResponseSuccess(warehouseSourceRawData) && + formik.values.product_warehouse_id + ? `Stok tersedia: ${formatNumber( + warehouseSourceRawData?.data?.find( + (item) => item.id === formik.values.product_warehouse_id + )?.quantity ?? 0 + )} ${ + warehouseSourceRawData?.data?.find( + (item) => item.id === formik.values.product_warehouse_id + )?.product?.uom?.name ?? '' + }` + : '' + } /> +
+
+
Date: Tue, 6 Jan 2026 16:06:26 +0700 Subject: [PATCH 22/32] fix(FE): shows delivery number when status marketing is delivery in marketing detail page --- .../marketing/detail/MarketingDetail.tsx | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/components/pages/marketing/detail/MarketingDetail.tsx b/src/components/pages/marketing/detail/MarketingDetail.tsx index 677ea422..12ebda20 100644 --- a/src/components/pages/marketing/detail/MarketingDetail.tsx +++ b/src/components/pages/marketing/detail/MarketingDetail.tsx @@ -124,7 +124,10 @@ const MarketingDetail = ({ return ( <>
- + 2 ? 'Delivery Order' : 'Sales Order'}`} + backUrl='/marketing' + /> {!isLoadingApproval && approvals && ( )} @@ -202,8 +205,23 @@ const MarketingDetail = ({ No. Sales Order : - {initialValues?.so_number} + + {initialValues?.so_number} + + {Number(initialValues?.latest_approval?.step_number) > 2 && ( + + + No. Delivery Order + + : + + {initialValues?.delivery_order + ?.map((item) => item.do_number) + .join(', ')} + + + )} Nama Pelanggan : @@ -230,12 +248,27 @@ const MarketingDetail = ({ {initialValues?.notes ?? '-'} - Dokumen + Dokumen Penjualan : + {Number(initialValues?.latest_approval?.step_number) > 2 && ( + + Dokumen Pengiriman + : + + {initialValues?.delivery_order?.map((item, index) => ( + + ))} + + + )}
From 8dfccf25d8703db1b92b35809aed03161eeb4a1c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 6 Jan 2026 19:32:13 +0700 Subject: [PATCH 23/32] refactor(FE): Truncate delivery document name in MovementForm --- src/components/pages/inventory/movement/form/MovementForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 3c49295e..64c87717 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -1562,7 +1562,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { width={20} height={20} /> - {delivery.document.name} + + {delivery.document.name} + ) : (
) : ( (initialValues?.chickins || []).map((chickin, index) => { - const isApproved = chickin.usage_qty !== 0; - const isPending = chickin.pending_usage_qty !== 0; + const isApproved = + initialValues.chickin_approval?.step_number === 2; + const isPending = initialValues.chickin_approval?.step_number === 1; const quantity = isApproved ? chickin.usage_qty : isPending @@ -146,18 +147,19 @@ const ChickinLogsView = ({ }) )} - {initialValues?.approval?.step_number <= 2 && ( - - - - )} + {initialValues.chickin_approval && + initialValues?.chickin_approval?.step_number < 2 && ( + + + + )} {chickinErrorMessage && (
setChickinErrorMessage('')}> diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 36ea90ca..20a8aff4 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -66,6 +66,7 @@ const ProjectFlockForm = ({ useState(''); const [selectedArea, setSelectedArea] = useState(''); const [selectedLocation, setSelectedLocation] = useState(''); + const [selectedCategory, setSelectedCategory] = useState(''); const [disabledLocation, setDisabledLocation] = useState( initialValues?.location?.id ? false : true ); @@ -125,7 +126,10 @@ const ProjectFlockForm = ({ const { options: optionsProductionStandards, isLoadingOptions: isLoadingProductionStandards, - } = useSelect(ProductionStandardApi.basePath, 'id', 'name'); + } = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', { + search: '', + project_category: selectedCategory, + }); const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ search: '', @@ -237,9 +241,19 @@ const ProjectFlockForm = ({ }; const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('category', (val as OptionType)?.value); + // Reset production standard when category is changed + formik.setFieldValue('production_standard_id', ''); + formik.setFieldValue('production_standard', ''); + formik.setFieldValue('category_option', val); - if (val == null) { + formik.setFieldValue('category', val ? (val as OptionType)?.value : ''); + + setSelectedCategory((val as OptionType)?.value as string); + + if (Boolean(val)) { + formik.setFieldTouched('category', false); + formik.setFieldError('category', ''); + } else { formik.setFieldTouched('category', true); } }; @@ -378,8 +392,6 @@ const ProjectFlockForm = ({ validationSchema: formType == 'add' ? ProjectFlockFormSchema : UpdateProjectFlockFormSchema, validateOnBlur: true, - // validateOnChange: true, - // validateOnMount: true, onSubmit: async (values) => { setProjectFlockFormErrorMessage(''); const payload: CreateProjectFlockPayload = { @@ -770,23 +782,6 @@ const ProjectFlockForm = ({ isClearable isDisabled={formType != 'add'} /> - { - optionChangeHandler(val, 'production_standard'); - }} - options={optionsProductionStandards} - isLoading={isLoadingProductionStandards} - isError={ - formik.touched.production_standard && - Boolean(formik.errors.production_standard) - } - errorMessage={formik.errors.production_standard as string} - isClearable - isDisabled={formType != 'add'} - /> + { + optionChangeHandler(val, 'production_standard'); + }} + options={optionsProductionStandards} + isLoading={isLoadingProductionStandards} + isError={ + formik.touched.production_standard_id && + Boolean(formik.errors.production_standard_id) + } + errorMessage={formik.errors.production_standard_id as string} + isClearable + isDisabled={formType != 'add'} + /> diff --git a/src/types/api/production/project-flock-kandang.d.ts b/src/types/api/production/project-flock-kandang.d.ts index 3a98a6e8..8c8d6273 100644 --- a/src/types/api/production/project-flock-kandang.d.ts +++ b/src/types/api/production/project-flock-kandang.d.ts @@ -10,9 +10,10 @@ export type BaseProjectFlockKandang = { kandang_id: number; kandang: Kandang; project_flock: ProjectFlock; - available_qtys?: AvailableQty[]; - chickins?: Chickin[]; approval: BaseApproval; + chickins?: Chickin[]; + available_qtys?: AvailableQty[]; + chickin_approval?: BaseApproval; }; export type AvailableQty = { @@ -56,14 +57,6 @@ export type ClosingExpense = { reference_number: string; }; -// "flag_name": "PAKAN", -// "product_warehouse_id": 14, -// "product_id": 8, -// "product_name": "281 SPECIAL STARTER", -// "product_category": "Bahan Baku", -// "uom": "Kilogram", -// "quantity": 1100 - export type StockItem = { flag_name: string; product_warehouse_id: number; From 13205ca80a2beee8f4b132c5a64a6060c5368fcf Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 8 Jan 2026 08:59:27 +0700 Subject: [PATCH 31/32] feat(FE): adding alert errors message for project flock and fixing bug approval status in chickin --- src/components/helper/form/FormErrors.tsx | 46 ++++++++++++ src/components/pages/ApprovalSteps.tsx | 2 +- .../form/InventoryAdjustmentForm.tsx | 1 + .../production/chickin/form/ChickinForm.tsx | 7 ++ .../chickin/form/tabs/ChickLogsView.tsx | 15 +++- .../form/ProjectFlockForm.schema.ts | 6 +- .../project-flock/form/ProjectFlockForm.tsx | 29 +++++++- src/lib/formik-helper.ts | 71 +++++++++++++++++++ 8 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 src/components/helper/form/FormErrors.tsx create mode 100644 src/lib/formik-helper.ts diff --git a/src/components/helper/form/FormErrors.tsx b/src/components/helper/form/FormErrors.tsx new file mode 100644 index 00000000..a351227f --- /dev/null +++ b/src/components/helper/form/FormErrors.tsx @@ -0,0 +1,46 @@ +import Alert from '@/components/Alert'; +import Button from '@/components/Button'; +import { Icon } from '@iconify/react'; + +/** + * Alert Unique Error List + * @param formErrorList - Array of error messages + * @param onClose - Function to close the alert + */ +const AlertErrorList = ({ + formErrorList, + onClose, +}: { + formErrorList: string[]; + onClose: () => void; +}) => { + return ( + +
+
+ + + Terdapat {formErrorList.length} error pada form: + +
+ +
+
    + {formErrorList.map((error, index) => ( +
  • + {error} +
  • + ))} +
+
+ ); +}; + +export default AlertErrorList; diff --git a/src/components/pages/ApprovalSteps.tsx b/src/components/pages/ApprovalSteps.tsx index 6ae7c13a..27d03da3 100644 --- a/src/components/pages/ApprovalSteps.tsx +++ b/src/components/pages/ApprovalSteps.tsx @@ -309,7 +309,7 @@ const useApprovalSteps = ({ moduleId: string; params?: { page?: number; - limit: number; + limit: number | string; search?: string; group_step_number?: boolean; }; diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index f134369e..525b81bb 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -125,6 +125,7 @@ const InventoryAdjustmentForm = ({ const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: '', + limit: '100', }).toString()}`; const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( warehouseUrl, diff --git a/src/components/pages/production/chickin/form/ChickinForm.tsx b/src/components/pages/production/chickin/form/ChickinForm.tsx index 8c613737..b5b1dc4d 100644 --- a/src/components/pages/production/chickin/form/ChickinForm.tsx +++ b/src/components/pages/production/chickin/form/ChickinForm.tsx @@ -18,6 +18,7 @@ import { Icon } from '@iconify/react'; import Badge from '@/components/Badge'; import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line'; import RequirePermission from '@/components/helper/RequirePermission'; +import { BaseApproval } from '@/types/api/api-general'; const ChickinFormKandang = ({ formType = 'add', initialValues, @@ -33,11 +34,16 @@ const ChickinFormKandang = ({ approvals, isLoading: approvalsLoading, refresh: refreshApprovals, + rawDataApprovals, } = useApprovalSteps({ latestApproval: initialValues?.chickin_approval, approvalLines: CHICKINS_APPROVAL_LINE, moduleName: 'CHICKINS', moduleId: initialValues?.id.toString() ?? '', + params: { + limit: 'limit', + group_step_number: false, + }, }); const afterSubmitFormChickin = () => { @@ -180,6 +186,7 @@ const ChickinFormKandang = ({
{openChickin && ( diff --git a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx index 80846565..e800ee68 100644 --- a/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx +++ b/src/components/pages/production/chickin/form/tabs/ChickLogsView.tsx @@ -8,6 +8,7 @@ import PillBadge from '@/components/PillBadge'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { formatDate, formatNumber } from '@/lib/helper'; import { ChickinApi } from '@/services/api/production/chickin'; +import { BaseApproval } from '@/types/api/api-general'; import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { Icon } from '@iconify/react'; import { useState } from 'react'; @@ -16,9 +17,11 @@ import toast from 'react-hot-toast'; const ChickinLogsView = ({ initialValues, afterSubmit, + rawDataApprovals, }: { initialValues: ProjectFlockKandang; afterSubmit?: () => void; + rawDataApprovals: BaseApproval[]; }) => { const confirmModal = useModal(); const [isApproveLoading, setIsApproveLoading] = useState(false); @@ -60,9 +63,15 @@ const ChickinLogsView = ({
) : ( (initialValues?.chickins || []).map((chickin, index) => { + const latestApproval = rawDataApprovals[0]; const isApproved = - initialValues.chickin_approval?.step_number === 2; - const isPending = initialValues.chickin_approval?.step_number === 1; + index == (initialValues?.chickins || []).length - 1 + ? latestApproval?.step_number === 2 + : true; + const isPending = + index == (initialValues?.chickins || []).length - 1 + ? latestApproval?.step_number === 1 + : false; const quantity = isApproved ? chickin.usage_qty : isPending @@ -82,7 +91,7 @@ const ChickinLogsView = ({ {/* Header with Status Badge */}
- Chick In #{index + 1} + Chick In #{index + 1} - {latestApproval?.step_number}
= diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 20a8aff4..7e90c94b 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -6,6 +6,8 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; import { AreaApi, FcrApi, @@ -64,6 +66,7 @@ const ProjectFlockForm = ({ const [projectFlockFormErrorMessage, setProjectFlockFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const [selectedArea, setSelectedArea] = useState(''); const [selectedLocation, setSelectedLocation] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); @@ -134,6 +137,7 @@ const ProjectFlockForm = ({ const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ search: '', location_id: selectedLocation == '' ? '0' : selectedLocation, + limit: 'limit', }).toString()}`; const { data: kandang, @@ -638,6 +642,17 @@ const ProjectFlockForm = ({ return !isNonstockAlreadyInBudgets; }); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + // Parse and display errors + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; // Stop submission + } + }; + return ( <>
@@ -697,7 +712,11 @@ const ProjectFlockForm = ({ { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }} onReset={formik.handleReset} > {/* Form Informasi Umum */} @@ -1063,6 +1082,14 @@ const ProjectFlockForm = ({
+ {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
{formType !== 'detail' && ( ( + errors: FormikErrors, + parentKey: string = '' +): ErrorMessage[] { + const errorList: ErrorMessage[] = []; + + Object.keys(errors).forEach((key) => { + const value = errors[key as keyof typeof errors]; + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if (typeof value === 'string') { + // Direct error message + errorList.push({ key: fullKey, message: value }); + } else if (Array.isArray(value)) { + // Array of errors + value.forEach((item, index) => { + if (typeof item === 'string') { + errorList.push({ key: `${fullKey}[${index}]`, message: item }); + } else if (item && typeof item === 'object') { + // Nested object in array + const nestedErrors = parseFormikErrors( + item as FormikErrors, + `${fullKey}[${index}]` + ); + errorList.push(...nestedErrors); + } + }); + } else if (value && typeof value === 'object') { + // Nested object + const nestedErrors = parseFormikErrors( + value as FormikErrors, + fullKey + ); + errorList.push(...nestedErrors); + } + }); + + return errorList; +} + +/** + * Get unique error messages from Formik errors + * @param errors - Formik errors object + * @returns Array of unique error messages + */ +export function getUniqueFormikErrors(errors: FormikErrors): string[] { + const errorList = parseFormikErrors(errors); + return Array.from(new Set(errorList.map((e) => e.message))); +} + +/** + * Get all error messages from Formik errors + * @param errors - Formik errors object + * @returns Array of error messages + */ +export function getAllFormikErrors(errors: FormikErrors): ErrorMessage[] { + return parseFormikErrors(errors); +} From 0ed30e184b1fc54c6031d3473eaeb6753f22f1d3 Mon Sep 17 00:00:00 2001 From: randy-ar Date: Thu, 8 Jan 2026 09:19:55 +0700 Subject: [PATCH 32/32] feat(FE): implement alert error list in marketing module --- .../pages/marketing/form/MarketingForm.tsx | 32 +++++++++++++++- .../delivery-order/DeliverOrderProduct.tsx | 35 ++++++++++++++--- .../sales-order/SalesOrderProduct.schema.ts | 12 ++++-- .../sales-order/SalesOrderProductForm.tsx | 38 ++++++++++++++++--- 4 files changed, 99 insertions(+), 18 deletions(-) diff --git a/src/components/pages/marketing/form/MarketingForm.tsx b/src/components/pages/marketing/form/MarketingForm.tsx index 1c5322e1..51c20d8e 100644 --- a/src/components/pages/marketing/form/MarketingForm.tsx +++ b/src/components/pages/marketing/form/MarketingForm.tsx @@ -48,6 +48,8 @@ import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema'; import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema'; import RequirePermission from '@/components/helper/RequirePermission'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable); const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm); @@ -217,6 +219,7 @@ const MarketingForm = ({ const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>( 'add' ); + const [formErrorList, setFormErrorList] = useState([]); const [deliveryOrderValues, setDeliveryOrderValues] = useState< DeliveryOrderProductFormValues[] >( @@ -558,11 +561,28 @@ const MarketingForm = ({ ); }, [memoSalesOrder]); + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + // Parse and display errors + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; // Stop submission + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(); + }; + return ( <>
+ {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} + {/* Form Actions */}
+ {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +
diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts index b2f42254..e62ed701 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts @@ -25,15 +25,19 @@ export const SalesOrderProductSchema: Yup.ObjectSchema { const [formErrorMessage, setFormErrorMessage] = useState(''); const [currentInput, setCurrentInput] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); // ============ Formik ============ const formik = useFormik({ @@ -169,15 +172,29 @@ const SalesOrderProductForm = ({ } }; + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + // Parse and display errors + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; // Stop submission + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleBlurField(currentInput); + handleValidateForm(); + formik.handleSubmit(e); + }; + return ( <> { - e.preventDefault(); - handleBlurField(currentInput); - formik.handleSubmit(e); - }} + onSubmit={handleFormSubmit} onReset={handleResetForm} > {formErrorMessage && ( @@ -338,6 +355,15 @@ const SalesOrderProductForm = ({ placeholder='Masukan Total Penjualan' />
+ + {/* Error List Alert */} + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )} +