From cdf0442a2b28df322677d4e7dc7651beede39ab4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Mar 2026 09:04:14 +0700 Subject: [PATCH 01/21] refactor(FE): Add transition restrictions for recording operations --- .../production/recording/RecordingTable.tsx | 65 ++++-- .../recording/form/RecordingForm.tsx | 213 ++++++++++++++---- .../production/recording/recording-utils.ts | 60 +++++ src/types/api/production/project-flock.d.ts | 1 + src/types/api/production/recording.d.ts | 2 +- 5 files changed, 279 insertions(+), 62 deletions(-) create mode 100644 src/components/pages/production/recording/recording-utils.ts diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 1b7a326d..3cd64344 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -21,6 +21,7 @@ import SelectInput, { useSelect } from '@/components/input/SelectInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; +import Tooltip from '@/components/Tooltip'; import { useFormik } from 'formik'; import { AreaApi } from '@/services/api/master-data'; import { LocationApi } from '@/services/api/master-data'; @@ -36,6 +37,7 @@ import { import RecordingTableSkeleton from '@/components/pages/production/recording/skeleton/RecordingTableSkeleton'; import Table from '@/components/Table'; import { type Recording } from '@/types/api/production/recording'; +import { getRecordingRestriction } from './recording-utils'; import { RecordingApi } from '@/services/api/production'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -105,30 +107,57 @@ const RowOptionsMenu = ({ }; const isRecordingEditable = (recording: Recording) => { - if ( - recording.executed_at && - recording.project_flock?.project_flock_category === 'GROWING' - ) { + const category = recording.project_flock?.project_flock_category; + const isTransition = recording.is_transition; + + const restriction = getRecordingRestriction( + category || 'GROWING', + isTransition + ); + + if (restriction.isLocked) { return false; } return true; }; + const getRecordingRestrictionInfo = (recording: Recording) => { + const category = recording.project_flock?.project_flock_category; + const isTransition = recording.is_transition; + + return getRecordingRestriction(category || 'GROWING', isTransition); + }; + const isApproved = isRecordingApproved(props.row.original); const isRejected = isRecordingRejected(props.row.original); const isEditable = isRecordingEditable(props.row.original); + const restrictionInfo = getRecordingRestrictionInfo(props.row.original); return (
- - - + + + + { cell: (props) => { const category = props.row.original.project_flock?.project_flock_category; + const isTransition = props.row.original.is_transition; if (!category) return '-'; const color = category === 'LAYING' ? 'info' : 'warning'; - return ; + return ( +
+ + {isTransition && ( + + (Transisi) + + )} +
+ ); }, }, { diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index af4ab78b..c744d768 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -70,7 +70,7 @@ import { } from '@/components/pages/production/recording/form/RecordingForm.schema'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; -import { formatDate, formatNumber } from '@/lib/helper'; +import { formatDate, formatNumber, cn } from '@/lib/helper'; import toast from 'react-hot-toast'; import ApprovalSteps, { useApprovalSteps, @@ -80,6 +80,7 @@ import { LAYING_RECORDING_APPROVAL_LINE, } from '@/config/approval-line'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import { getRecordingRestriction } from '../recording-utils'; interface RecordingFormProps { type?: 'add' | 'edit' | 'detail'; @@ -272,16 +273,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return recording?.approval?.action === 'REJECTED'; }, []); - const isRecordingEditable = useCallback((recording?: Recording) => { - if ( - recording?.executed_at && - recording?.project_flock?.project_flock_category === 'GROWING' - ) { - return false; - } - return true; - }, []); - // ===== PAYLOAD CREATION HELPERS ===== const createGrowingPayload = useCallback( (values: RecordingGrowingFormValues) => { @@ -476,6 +467,60 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ? projectFlockKandangDetailData.data : undefined; + // ===== TRANSITION RESTRICTION LOGIC ===== + const isTransitionPeriod = useMemo(() => { + return initialValues?.is_transition ?? false; + }, [initialValues]); + + const recordingRestriction = useMemo(() => { + const category = + initialValues?.project_flock?.project_flock_category || + projectFlockKandangLookup?.project_flock?.category || + projectFlockKandangDetail?.project_flock?.category || + 'GROWING'; + + const isTransition = initialValues?.is_transition ?? false; + + const currentFlockCategory = projectFlockKandangDetail?.project_flock + ?.category as 'GROWING' | 'LAYING' | undefined; + + return getRecordingRestriction( + category as 'GROWING' | 'LAYING', + isTransition, + type === 'edit' ? currentFlockCategory : undefined + ); + }, [ + initialValues, + projectFlockKandangLookup, + projectFlockKandangDetail, + type, + ]); + + const isRecordingEditable = useCallback( + (recording?: Recording) => { + if (!recording) return true; + + const category = recording.project_flock?.project_flock_category; + const isTransition = recording.is_transition; + + const currentFlockCategory = projectFlockKandangDetail?.project_flock + ?.category as 'GROWING' | 'LAYING' | undefined; + + const restriction = getRecordingRestriction( + category || 'GROWING', + isTransition, + currentFlockCategory + ); + + if (restriction.isLocked) { + return false; + } + + return true; + }, + [projectFlockKandangDetail] + ); + const { options: stockProductOptions, rawData: stockProducts, @@ -2324,6 +2369,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedStocks([]); } }} + disabled={!recordingRestriction.canEditStock} classNames={{ wrapper: 'flex justify-center', checkbox: 'checkbox checkbox-sm', @@ -2373,6 +2419,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); } }} + disabled={!recordingRestriction.canEditStock} classNames={{ wrapper: 'flex justify-center', checkbox: 'checkbox checkbox-sm', @@ -2425,7 +2472,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { isSearchable isDisabled={ type === 'detail' || - !formik.values.project_flock_kandang_id + !formik.values.project_flock_kandang_id || + !recordingRestriction.canEditStock } isClearable={type !== 'detail'} inputPrefix={ @@ -2472,7 +2520,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ) : null } - disabled={type === 'detail'} + disabled={ + type === 'detail' || + !recordingRestriction.canEditStock + } /> {getStockUsageAdornment(idx)}
@@ -2484,6 +2535,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { type='button' color='error' onClick={() => removeStock(idx)} + disabled={!recordingRestriction.canEditStock} > { {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
- {selectedStocks.length > 0 && ( + {selectedStocks.length > 0 && + recordingRestriction.canEditStock && ( + + )} + - )} - +
)} + {/* Transition Warning Banner -- MOVED UP -- */} + {isTransitionPeriod && ( +
+ + + {isLayingCategory + ? 'Masa Transisi Laying: Hanya Deplesi yang dapat diisi. Stock (Pakan/OVK) tidak dapat diinput.' + : 'Masa Transisi Growing: Hanya Stock (Pakan/OVK) yang dapat diisi. Deplesi tidak dapat diinput.'} + +
+ )} + + {/* Locked Recording Warning */} + {recordingRestriction.isLocked && ( +
+ + {recordingRestriction.lockReason} +
+ )} + {/* Depletions Table */} {((type as 'add' | 'edit' | 'detail') !== 'detail' || (formik.values.depletions?.length ?? 0) > 0) && ( @@ -2562,6 +2657,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedDepletions([]); } }} + disabled={!recordingRestriction.canEditDepletion} classNames={{ wrapper: 'flex justify-center', checkbox: 'checkbox checkbox-sm', @@ -2598,6 +2694,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); } }} + disabled={!recordingRestriction.canEditDepletion} classNames={{ wrapper: 'flex justify-center', checkbox: 'checkbox checkbox-sm', @@ -2640,7 +2737,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { idx ).errorMessage } - isDisabled={type === 'detail'} + isDisabled={ + type === 'detail' || + !recordingRestriction.canEditDepletion + } className={{ wrapper: 'w-full min-w-48', }} @@ -2679,7 +2779,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ) : null } - disabled={type === 'detail'} + disabled={ + type === 'detail' || + !recordingRestriction.canEditDepletion + } /> {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( @@ -2689,6 +2792,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { type='button' color='error' onClick={() => removeDepletion(idx)} + disabled={ + !recordingRestriction.canEditDepletion + } > { {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
- {selectedDepletions.length > 0 && ( + {selectedDepletions.length > 0 && + recordingRestriction.canEditDepletion && ( + + )} + - )} - +
)}
diff --git a/src/components/pages/production/recording/recording-utils.ts b/src/components/pages/production/recording/recording-utils.ts new file mode 100644 index 00000000..3b7530c9 --- /dev/null +++ b/src/components/pages/production/recording/recording-utils.ts @@ -0,0 +1,60 @@ +export type RecordingRestriction = { + canEditStock: boolean; + canEditDepletion: boolean; + canEditEgg: boolean; + isLocked: boolean; + lockReason?: string; +}; + +export const getRecordingRestriction = ( + category: 'GROWING' | 'LAYING', + isTransition: boolean, + currentCategory?: 'GROWING' | 'LAYING' +): RecordingRestriction => { + if (currentCategory === 'LAYING' && category === 'GROWING') { + return { + canEditStock: false, + canEditDepletion: false, + canEditEgg: false, + isLocked: true, + lockReason: + 'Recording Growing telah terkunci karena Project Flock sudah masuk fase Laying', + }; + } + + if (category === 'GROWING') { + if (isTransition) { + return { + canEditStock: true, + canEditDepletion: false, + canEditEgg: false, + isLocked: false, + lockReason: undefined, + }; + } + return { + canEditStock: true, + canEditDepletion: true, + canEditEgg: false, + isLocked: false, + lockReason: undefined, + }; + } + + if (isTransition) { + return { + canEditStock: false, + canEditDepletion: true, + canEditEgg: false, + isLocked: false, + lockReason: undefined, + }; + } + return { + canEditStock: true, + canEditDepletion: true, + canEditEgg: true, + isLocked: false, + lockReason: undefined, + }; +}; diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 204e7b49..557aebc8 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -74,6 +74,7 @@ export type ProjectFlockKandangLookup = { available_quantity?: number; population: number; chick_in_date: string; + is_transition: boolean; }; export type ProjectFlockAvailableQuantity = { diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index b78642b8..23093169 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -49,7 +49,7 @@ export type BaseRecording = { project_flock: ProjectFlock; record_datetime: string; day: number; - executed_at: string; + is_transition: boolean; } & ProductionMetrics; export type RecordingDepletion = { From ed34a991178b38230fe1415c327916cac9801444 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Mar 2026 13:59:24 +0700 Subject: [PATCH 02/21] refactor(FE): Refactor to use `is_laying` instead of `project_flock_category` --- .../production/recording/RecordingTable.tsx | 19 ++---- .../recording/form/RecordingForm.tsx | 59 ++++++++----------- .../production/recording/recording-utils.ts | 8 +-- src/types/api/production/recording.d.ts | 1 + 4 files changed, 34 insertions(+), 53 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 3cd64344..e9d260ce 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -107,12 +107,9 @@ const RowOptionsMenu = ({ }; const isRecordingEditable = (recording: Recording) => { - const category = recording.project_flock?.project_flock_category; - const isTransition = recording.is_transition; - const restriction = getRecordingRestriction( - category || 'GROWING', - isTransition + recording.is_laying, + recording.is_transition ); if (restriction.isLocked) { @@ -122,10 +119,7 @@ const RowOptionsMenu = ({ }; const getRecordingRestrictionInfo = (recording: Recording) => { - const category = recording.project_flock?.project_flock_category; - const isTransition = recording.is_transition; - - return getRecordingRestriction(category || 'GROWING', isTransition); + return getRecordingRestriction(recording.is_laying, recording.is_transition); }; const isApproved = isRecordingApproved(props.row.original); @@ -790,11 +784,10 @@ const RecordingTable = () => { { header: 'Kategori', cell: (props) => { - const category = - props.row.original.project_flock?.project_flock_category; + const isLaying = props.row.original.is_laying; const isTransition = props.row.original.is_transition; - if (!category) return '-'; - const color = category === 'LAYING' ? 'info' : 'warning'; + const category = isLaying ? 'LAYING' : 'GROWING'; + const color = isLaying ? 'info' : 'warning'; return (
diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index c744d768..274080e3 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -473,21 +473,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [initialValues]); const recordingRestriction = useMemo(() => { - const category = - initialValues?.project_flock?.project_flock_category || - projectFlockKandangLookup?.project_flock?.category || - projectFlockKandangDetail?.project_flock?.category || - 'GROWING'; + const isLaying = + initialValues?.is_laying ?? + (projectFlockKandangLookup?.project_flock?.category === 'LAYING' || + projectFlockKandangDetail?.project_flock?.category === 'LAYING' || + false); const isTransition = initialValues?.is_transition ?? false; - const currentFlockCategory = projectFlockKandangDetail?.project_flock - ?.category as 'GROWING' | 'LAYING' | undefined; + const currentIsLaying = + projectFlockKandangDetail?.project_flock?.category === 'LAYING'; return getRecordingRestriction( - category as 'GROWING' | 'LAYING', + isLaying, isTransition, - type === 'edit' ? currentFlockCategory : undefined + type === 'edit' ? currentIsLaying : undefined ); }, [ initialValues, @@ -500,16 +500,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { (recording?: Recording) => { if (!recording) return true; - const category = recording.project_flock?.project_flock_category; - const isTransition = recording.is_transition; - - const currentFlockCategory = projectFlockKandangDetail?.project_flock - ?.category as 'GROWING' | 'LAYING' | undefined; + const currentIsLaying = + projectFlockKandangDetail?.project_flock?.category === 'LAYING'; const restriction = getRecordingRestriction( - category || 'GROWING', - isTransition, - currentFlockCategory + recording.is_laying, + recording.is_transition, + currentIsLaying ); if (restriction.isLocked) { @@ -627,14 +624,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [approvedProjectFlockKandangsData]); const isLayingCategory = - initialValues?.project_flock?.project_flock_category === 'LAYING' || - projectFlockKandangLookup?.project_flock?.category === 'LAYING' || - projectFlockKandangDetail?.project_flock?.category === 'LAYING'; + initialValues?.is_laying ?? + (projectFlockKandangLookup?.project_flock?.category === 'LAYING' || + projectFlockKandangDetail?.project_flock?.category === 'LAYING' || + false); - const isGrowingCategory = - initialValues?.project_flock?.project_flock_category === 'GROWING' || - projectFlockKandangLookup?.project_flock?.category === 'GROWING' || - projectFlockKandangDetail?.project_flock?.category === 'GROWING'; + const isGrowingCategory = !isLayingCategory; const recordingApprovalLines = useMemo(() => { if (isLayingCategory) { @@ -2003,15 +1998,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {

- {initialValues.project_flock?.project_flock_category} + {initialValues.is_laying ? 'LAYING' : 'GROWING'}

@@ -2148,9 +2138,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {type === 'detail' && initialValues && (
{/* FCR Section */} @@ -2241,8 +2229,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Egg Production Section - Only for LAYING category */} {type === 'detail' && initialValues && - initialValues.project_flock?.project_flock_category === - 'LAYING' && ( + initialValues.is_laying && (
diff --git a/src/components/pages/production/recording/recording-utils.ts b/src/components/pages/production/recording/recording-utils.ts index 3b7530c9..53bd94ad 100644 --- a/src/components/pages/production/recording/recording-utils.ts +++ b/src/components/pages/production/recording/recording-utils.ts @@ -7,11 +7,11 @@ export type RecordingRestriction = { }; export const getRecordingRestriction = ( - category: 'GROWING' | 'LAYING', + isLaying: boolean, isTransition: boolean, - currentCategory?: 'GROWING' | 'LAYING' + currentIsLaying?: boolean ): RecordingRestriction => { - if (currentCategory === 'LAYING' && category === 'GROWING') { + if (currentIsLaying && !isLaying) { return { canEditStock: false, canEditDepletion: false, @@ -22,7 +22,7 @@ export const getRecordingRestriction = ( }; } - if (category === 'GROWING') { + if (!isLaying) { if (isTransition) { return { canEditStock: true, diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 23093169..8ce0ef15 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -50,6 +50,7 @@ export type BaseRecording = { record_datetime: string; day: number; is_transition: boolean; + is_laying: boolean; } & ProductionMetrics; export type RecordingDepletion = { From f1a952ca6b86a3e51c6598f5edc32fa22f2a04a1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Mar 2026 14:01:29 +0700 Subject: [PATCH 03/21] refactor(FE): Refactor getRecordingRestrictionInfo for better readability --- src/components/pages/production/recording/RecordingTable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index e9d260ce..c6e01cc4 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -119,7 +119,10 @@ const RowOptionsMenu = ({ }; const getRecordingRestrictionInfo = (recording: Recording) => { - return getRecordingRestriction(recording.is_laying, recording.is_transition); + return getRecordingRestriction( + recording.is_laying, + recording.is_transition + ); }; const isApproved = isRecordingApproved(props.row.original); From 0929461ec5ef74ab51a13df833e2988105dd31c5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Mar 2026 15:03:42 +0700 Subject: [PATCH 04/21] refactor(FE): Improve transition and laying state handling in RecordingForm --- .../recording/form/RecordingForm.tsx | 25 ++++++++++++------- src/types/api/production/project-flock.d.ts | 1 + 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 274080e3..ff97d1ae 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -469,17 +469,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { // ===== TRANSITION RESTRICTION LOGIC ===== const isTransitionPeriod = useMemo(() => { - return initialValues?.is_transition ?? false; - }, [initialValues]); + return ( + initialValues?.is_transition ?? + projectFlockKandangLookup?.is_transition ?? + false + ); + }, [initialValues, projectFlockKandangLookup]); const recordingRestriction = useMemo(() => { const isLaying = initialValues?.is_laying ?? - (projectFlockKandangLookup?.project_flock?.category === 'LAYING' || - projectFlockKandangDetail?.project_flock?.category === 'LAYING' || - false); + projectFlockKandangLookup?.is_laying ?? + projectFlockKandangDetail?.project_flock?.category === 'LAYING' || + false; - const isTransition = initialValues?.is_transition ?? false; + const isTransition = + initialValues?.is_transition ?? + projectFlockKandangLookup?.is_transition ?? + false; const currentIsLaying = projectFlockKandangDetail?.project_flock?.category === 'LAYING'; @@ -625,9 +632,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const isLayingCategory = initialValues?.is_laying ?? - (projectFlockKandangLookup?.project_flock?.category === 'LAYING' || - projectFlockKandangDetail?.project_flock?.category === 'LAYING' || - false); + projectFlockKandangLookup?.is_laying ?? + projectFlockKandangDetail?.project_flock?.category === 'LAYING' || + false; const isGrowingCategory = !isLayingCategory; diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 557aebc8..41a6a1c0 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -75,6 +75,7 @@ export type ProjectFlockKandangLookup = { population: number; chick_in_date: string; is_transition: boolean; + is_laying: boolean; }; export type ProjectFlockAvailableQuantity = { From cc08e3af152af9906dad08a9b03656dc83f26ce6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Mar 2026 15:04:58 +0700 Subject: [PATCH 05/21] refactor(FE): Fix logical grouping in isLaying and isLayingCategory checks --- .../pages/production/recording/form/RecordingForm.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index ff97d1ae..3a247001 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -480,8 +480,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const isLaying = initialValues?.is_laying ?? projectFlockKandangLookup?.is_laying ?? - projectFlockKandangDetail?.project_flock?.category === 'LAYING' || - false; + (projectFlockKandangDetail?.project_flock?.category === 'LAYING' || + false); const isTransition = initialValues?.is_transition ?? @@ -633,8 +633,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const isLayingCategory = initialValues?.is_laying ?? projectFlockKandangLookup?.is_laying ?? - projectFlockKandangDetail?.project_flock?.category === 'LAYING' || - false; + (projectFlockKandangDetail?.project_flock?.category === 'LAYING' || false); const isGrowingCategory = !isLayingCategory; From 058f9f403d757f9b55fba5c6a4da37581bd53b33 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Mar 2026 11:13:46 +0700 Subject: [PATCH 06/21] refactor(FE): Improve delete handlers with success and error feedback --- .../pages/production/recording/RecordingTable.tsx | 11 ++++++++--- .../production/recording/form/RecordingForm.tsx | 12 +++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index c6e01cc4..18fe6ae8 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -586,12 +586,17 @@ const RecordingTable = () => { const singleDeleteHandler = async () => { setIsDeleteLoading(true); - await RecordingApi.delete(selectedRecording?.id as number); - refreshRecordings(); + const response = await RecordingApi.delete(selectedRecording?.id as number); singleDeleteModal.closeModal(); - toast.success('Successfully delete Recording!'); setIsDeleteLoading(false); + + if (isResponseSuccess(response)) { + toast.success(response?.message || 'Successfully delete Recording!'); + refreshRecordings(); + } else { + toast.error(response?.message || 'Failed to delete Recording'); + } }; const approveHandler = async (notes: string) => { diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 3a247001..226acd20 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -371,11 +371,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (!initialValues?.id) return; setIsDeleteLoading(true); - await RecordingApi.delete(initialValues.id); + const response = await RecordingApi.delete(initialValues.id); + deleteModal.closeModal(); - toast.success('Successfully delete Recording!'); setIsDeleteLoading(false); - router.push('/production/recording'); + + if (isResponseSuccess(response)) { + toast.success(response?.message || 'Successfully delete Recording!'); + router.push('/production/recording'); + } else { + toast.error(response?.message || 'Failed to delete Recording'); + } }, [deleteModal, initialValues?.id, router]); // ===== API DATA FETCHING ===== From ebe7c367e7bedefe2de4c0b9bd393faa56a36050 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Mar 2026 11:34:14 +0700 Subject: [PATCH 07/21] refactor(FE): Refactor isLaying logic for clarity and reuse --- .../recording/form/RecordingForm.tsx | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 226acd20..3a1bd01f 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -483,11 +483,19 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [initialValues, projectFlockKandangLookup]); const recordingRestriction = useMemo(() => { - const isLaying = - initialValues?.is_laying ?? - projectFlockKandangLookup?.is_laying ?? - (projectFlockKandangDetail?.project_flock?.category === 'LAYING' || - false); + // Determine isLaying - check both is_laying flag AND category + let isLaying: boolean; + if (initialValues?.is_laying !== undefined) { + isLaying = initialValues.is_laying; + } else if (projectFlockKandangLookup) { + isLaying = + projectFlockKandangLookup.is_laying || + projectFlockKandangLookup.project_flock?.category === 'LAYING'; + } else { + isLaying = + projectFlockKandangDetail?.project_flock?.category === 'LAYING' || + false; + } const isTransition = initialValues?.is_transition ?? @@ -636,10 +644,29 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return approvedProjectFlockKandangsData.data; }, [approvedProjectFlockKandangsData]); - const isLayingCategory = - initialValues?.is_laying ?? - projectFlockKandangLookup?.is_laying ?? - (projectFlockKandangDetail?.project_flock?.category === 'LAYING' || false); + const isLayingCategory = useMemo(() => { + // Priority 1: initialValues (for edit/detail mode) + if (initialValues?.is_laying !== undefined) { + return initialValues.is_laying; + } + + // Priority 2: projectFlockKandangLookup (for add mode with lookup) + if (projectFlockKandangLookup) { + return ( + projectFlockKandangLookup.is_laying || + projectFlockKandangLookup.project_flock?.category === 'LAYING' + ); + } + + // Priority 3: projectFlockKandangDetail (fallback for edit/detail mode) + return ( + projectFlockKandangDetail?.project_flock?.category === 'LAYING' || false + ); + }, [ + initialValues?.is_laying, + projectFlockKandangLookup, + projectFlockKandangDetail, + ]); const isGrowingCategory = !isLayingCategory; From aa13e989c1ebd9437d6bba62e26867b44fdbec31 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Mar 2026 11:40:10 +0700 Subject: [PATCH 08/21] feat(FE): Add week calculation utility and improve state resets --- .../recording/form/RecordingForm.tsx | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 3a1bd01f..2a2262b0 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -243,6 +243,23 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [isProductionStandardModalOpen, setIsProductionStandardModalOpen] = useState(false); + const calculateWeek = useCallback( + (day: number): number => { + if ( + productionStandards?.details && + productionStandards.details.length > 0 + ) { + const firstWeek = productionStandards.details[0].week; + + const weekOffset = Math.ceil(day / 7) - 1; + return firstWeek + weekOffset; + } + + return Math.ceil(day / 7); + }, + [productionStandards] + ); + useEffect(() => { const checkProductionStandardModalOpen = () => { const isOpen = productionStandardModal.ref.current?.open || false; @@ -441,13 +458,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { () => ProductionStandardApi.getSingle(productionStandardId!) ); + const { data: productionStandardForAdd } = useSWR( + type === 'add' && productionStandardId + ? `production-standard-add-${productionStandardId}` + : null, + () => ProductionStandardApi.getSingle(productionStandardId!) + ); + useEffect(() => { if (productionStandard?.status === 'success') { setProductionStandards( productionStandard.data as ProductionStandard | null ); + } else if (productionStandardForAdd?.status === 'success') { + setProductionStandards( + productionStandardForAdd.data as ProductionStandard | null + ); } - }, [productionStandard]); + }, [productionStandard, productionStandardForAdd]); const projectFlockKandangDetailUrl = useMemo(() => { if ( @@ -1378,6 +1406,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedLocation(location); setSelectedProjectFlock(null); setSelectedKandang(null); + setProductionStandards(null); + setNextDayRecording(null); if (duplicateErrorShown) { toast.dismiss(); setDuplicateErrorShown(false); @@ -1402,6 +1432,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedProjectFlock(projectFlock); setSelectedKandang(null); + setProductionStandards(null); + setNextDayRecording(null); if (duplicateErrorShown) { toast.dismiss(); setDuplicateErrorShown(false); @@ -1422,6 +1454,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { formik.setFieldValue('kandang_id', kandangId); setSelectedKandang(kandang); + setProductionStandards(null); + setNextDayRecording(null); if (duplicateErrorShown) { toast.dismiss(); setDuplicateErrorShown(false); @@ -1958,10 +1992,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {

{type === 'add' ? nextDayRecording - ? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${Math.ceil(nextDayRecording.next_day / 7)})` + ? `Hari ke-${nextDayRecording.next_day} (Minggu ke-${calculateWeek(nextDayRecording.next_day)})` : '-' : initialValues?.day - ? `Hari ke-${initialValues.day} (Minggu ke-${Math.ceil(initialValues.day / 7)})` + ? `Hari ke-${initialValues.day} (Minggu ke-${calculateWeek(initialValues.day)})` : '-'}

From 44a5c510230b8c656d32db30568315b8a4f3f4c7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Mar 2026 14:02:07 +0700 Subject: [PATCH 09/21] refactor(FE): Refactor recording restriction logic for clarity and accuracy --- .../production/recording/RecordingTable.tsx | 12 +++++- .../recording/form/RecordingForm.tsx | 31 +++++++-------- .../production/recording/recording-utils.ts | 39 ++++++++++--------- 3 files changed, 44 insertions(+), 38 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 18fe6ae8..6ca8bb89 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -107,9 +107,13 @@ const RowOptionsMenu = ({ }; const isRecordingEditable = (recording: Recording) => { + const currentIsLaying = + recording.project_flock?.project_flock_category === 'LAYING'; + const restriction = getRecordingRestriction( recording.is_laying, - recording.is_transition + recording.is_transition, + currentIsLaying ); if (restriction.isLocked) { @@ -119,9 +123,13 @@ const RowOptionsMenu = ({ }; const getRecordingRestrictionInfo = (recording: Recording) => { + const currentIsLaying = + recording.project_flock?.project_flock_category === 'LAYING'; + return getRecordingRestriction( recording.is_laying, - recording.is_transition + recording.is_transition, + currentIsLaying ); }; diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 2a2262b0..e25f0f67 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -545,27 +545,24 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { type, ]); - const isRecordingEditable = useCallback( - (recording?: Recording) => { - if (!recording) return true; + const isRecordingEditable = useCallback((recording?: Recording) => { + if (!recording) return true; - const currentIsLaying = - projectFlockKandangDetail?.project_flock?.category === 'LAYING'; + const currentIsLaying = + recording.project_flock?.project_flock_category === 'LAYING'; - const restriction = getRecordingRestriction( - recording.is_laying, - recording.is_transition, - currentIsLaying - ); + const restriction = getRecordingRestriction( + recording.is_laying, + recording.is_transition, + currentIsLaying + ); - if (restriction.isLocked) { - return false; - } + if (restriction.isLocked) { + return false; + } - return true; - }, - [projectFlockKandangDetail] - ); + return true; + }, []); const { options: stockProductOptions, diff --git a/src/components/pages/production/recording/recording-utils.ts b/src/components/pages/production/recording/recording-utils.ts index 53bd94ad..c1fc279d 100644 --- a/src/components/pages/production/recording/recording-utils.ts +++ b/src/components/pages/production/recording/recording-utils.ts @@ -22,16 +22,17 @@ export const getRecordingRestriction = ( }; } - if (!isLaying) { - if (isTransition) { - return { - canEditStock: true, - canEditDepletion: false, - canEditEgg: false, - isLocked: false, - lockReason: undefined, - }; - } + if (isTransition && !isLaying) { + return { + canEditStock: true, + canEditDepletion: false, + canEditEgg: false, + isLocked: false, + lockReason: undefined, + }; + } + + if (!isLaying && !isTransition) { return { canEditStock: true, canEditDepletion: true, @@ -40,21 +41,21 @@ export const getRecordingRestriction = ( lockReason: undefined, }; } - - if (isTransition) { + if (isLaying && !isTransition) { return { - canEditStock: false, + canEditStock: true, canEditDepletion: true, - canEditEgg: false, + canEditEgg: true, isLocked: false, lockReason: undefined, }; } + return { - canEditStock: true, - canEditDepletion: true, - canEditEgg: true, - isLocked: false, - lockReason: undefined, + canEditStock: false, + canEditDepletion: false, + canEditEgg: false, + isLocked: true, + lockReason: 'Kondisi transisi tidak valid', }; }; From 1b499bc967f47d3ffc56afbcf776cc630a83fa1b Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 10 Mar 2026 17:04:44 +0700 Subject: [PATCH 10/21] implement transition recording --- .../production/recording/RecordingTable.tsx | 28 +++++++++++++++++-- .../recording/form/RecordingForm.tsx | 26 ++++++++++++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 6ca8bb89..a83c8d38 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -107,6 +107,13 @@ const RowOptionsMenu = ({ }; const isRecordingEditable = (recording: Recording) => { + const isGrowingCategory = + recording.project_flock?.project_flock_category === 'GROWING'; + const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying; + if (isGrowingLockedByLaying) { + return false; + } + const currentIsLaying = recording.project_flock?.project_flock_category === 'LAYING'; @@ -123,6 +130,20 @@ const RowOptionsMenu = ({ }; const getRecordingRestrictionInfo = (recording: Recording) => { + const isGrowingCategory = + recording.project_flock?.project_flock_category === 'GROWING'; + const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying; + if (isGrowingLockedByLaying) { + return { + canEditStock: false, + canEditDepletion: false, + canEditEgg: false, + isLocked: true, + lockReason: + 'Recording Growing tidak dapat diubah karena sudah masuk fase laying dan dipakai pada recording laying', + }; + } + const currentIsLaying = recording.project_flock?.project_flock_category === 'LAYING'; @@ -800,10 +821,11 @@ const RecordingTable = () => { { header: 'Kategori', cell: (props) => { - const isLaying = props.row.original.is_laying; const isTransition = props.row.original.is_transition; - const category = isLaying ? 'LAYING' : 'GROWING'; - const color = isLaying ? 'info' : 'warning'; + const category = + props.row.original.project_flock?.project_flock_category || + 'GROWING'; + const color = category === 'LAYING' ? 'info' : 'warning'; return (
diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index e25f0f67..885c1e1a 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -420,13 +420,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }); const projectFlockKandangLookupUrl = useMemo(() => { - if (!selectedProjectFlock || !selectedKandang) return null; + if (!selectedProjectFlock || !selectedKandang || !selectedRecordDate) + return null; const params = new URLSearchParams({ project_flock_id: selectedProjectFlock.value.toString(), kandang_id: selectedKandang.value.toString(), + record_date: selectedRecordDate, }); return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; - }, [selectedProjectFlock, selectedKandang]); + }, [selectedProjectFlock, selectedKandang, selectedRecordDate]); const { data: projectFlockKandangLookupData } = useSWR( projectFlockKandangLookupUrl, @@ -548,6 +550,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const isRecordingEditable = useCallback((recording?: Recording) => { if (!recording) return true; + const isGrowingCategory = + recording.project_flock?.project_flock_category === 'GROWING'; + const isGrowingLockedByLaying = isGrowingCategory && recording.is_laying; + if (isGrowingLockedByLaying) { + return false; + } + const currentIsLaying = recording.project_flock?.project_flock_category === 'LAYING'; @@ -2066,13 +2075,22 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
Kategori

+ {(() => { + const category = + initialValues.project_flock?.project_flock_category || + 'GROWING'; + const color = + category === 'LAYING' ? 'info' : 'warning'; + return ( - {initialValues.is_laying ? 'LAYING' : 'GROWING'} + {category} + ); + })()}

From 55407871549e15634204486c6f1a14def423a6d7 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 10 Mar 2026 17:05:42 +0700 Subject: [PATCH 11/21] implement transition recording --- .../pages/production/recording/form/RecordingForm.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 885c1e1a..387bd5da 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2082,13 +2082,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const color = category === 'LAYING' ? 'info' : 'warning'; return ( - - {category} - + + {category} + ); })()}

From 1621f2ab7d0a77352ce8a1aed06b20fffbe2ba0d Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 10 Mar 2026 17:34:52 +0700 Subject: [PATCH 12/21] fixing disable field detail --- .../production/recording/RecordingTable.tsx | 14 +++----------- .../production/recording/form/RecordingForm.tsx | 17 ++++++----------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index a83c8d38..d760c313 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -173,15 +173,7 @@ const RowOptionsMenu = ({ anchorName={popoverAnchorName} className={restrictionInfo.isLocked ? 'text-error' : ''} > - + @@ -218,7 +210,7 @@ const RowOptionsMenu = ({ )} - {!isApproved && !isRejected && ( + {!restrictionInfo.isLocked && !isApproved && !isRejected && ( )} - {!isApproved && !isRejected && ( + {!restrictionInfo.isLocked && !isApproved && !isRejected && ( )} - {!restrictionInfo.isLocked && !isApproved && !isRejected && ( + {!isApproved && !isRejected && ( )} - {!restrictionInfo.isLocked && !isApproved && !isRejected && ( + {!isApproved && !isRejected && (
)} - {/* Stocks Table */} - -
- - - - {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - - )} - - - {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - - )} - - - - {formik.values.stocks?.map((stock, idx) => ( - + {/* Stocks Table - Only show if can edit stock or has data */} + {(recordingRestriction.canEditStock || + (type === 'detail' && formik.values.stocks?.length > 0)) && ( + +
+
- 0 - } - onChange={( - e: React.ChangeEvent - ) => { - if (e.target.checked) { - setSelectedStocks( - formik.values.stocks?.map((_, idx) => idx) ?? [] - ); - } else { - setSelectedStocks([]); - } - }} - disabled={!recordingRestriction.canEditStock} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> - - Persediaan - - * - - - Jumlah Pakai - - * - - Action
+ + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - )} - - + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( - + )} - ))} - -
+ 0 + } onChange={( e: React.ChangeEvent ) => { if (e.target.checked) { - setSelectedStocks([...selectedStocks, idx]); - } else { setSelectedStocks( - selectedStocks.filter((i) => i !== idx) + formik.values.stocks?.map((_, idx) => idx) ?? + [] ); + } else { + setSelectedStocks([]); } }} disabled={!recordingRestriction.canEditStock} @@ -2531,169 +2494,221 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { checkbox: 'checkbox checkbox-sm', }} /> - + - - product.value === stock.product_warehouse_id - ) || null - } - onChange={(selectedOption) => { - const option = selectedOption as OptionType | null; - formik.setFieldValue( - `stocks.${idx}.product_warehouse_id`, - option?.value || 0 - ); - }} - options={getAvailableStockProductOptions(idx)} - placeholder={ - !formik.values.project_flock_kandang_id - ? 'Pilih kandang terlebih dahulu' - : 'Pilih Produk' - } - isLoading={isLoadingStockProducts} - onMenuScrollToBottom={loadMoreStockProducts} - isError={ - isRepeaterInputError( - 'stocks', - 'product_warehouse_id', - idx - ).isError - } - errorMessage={ - isRepeaterInputError( - 'stocks', - 'product_warehouse_id', - idx - ).errorMessage - } - className={{ - wrapper: 'w-full min-w-48', - }} - isSearchable - isDisabled={ - type === 'detail' || - !formik.values.project_flock_kandang_id || - !recordingRestriction.canEditStock - } - isClearable={type !== 'detail'} - inputPrefix={ - stock.product_warehouse_id - ? getProductFlagBadgeAdornment( - stock.product_warehouse_id - ) - : undefined - } - /> - -
- - {getStockUsageAdornment(idx)} -
-
+ Persediaan + + * + + + Jumlah Pakai + + * + + -
- -
-
Action
-
- {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( -
- {selectedStocks.length > 0 && - recordingRestriction.canEditStock && ( + + + {formik.values.stocks?.map((stock, idx) => ( + + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + + + ) => { + if (e.target.checked) { + setSelectedStocks([...selectedStocks, idx]); + } else { + setSelectedStocks( + selectedStocks.filter((i) => i !== idx) + ); + } + }} + disabled={!recordingRestriction.canEditStock} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> + + )} + + + product.value === stock.product_warehouse_id + ) || null + } + onChange={(selectedOption) => { + const option = + selectedOption as OptionType | null; + formik.setFieldValue( + `stocks.${idx}.product_warehouse_id`, + option?.value || 0 + ); + }} + options={getAvailableStockProductOptions(idx)} + placeholder={ + !formik.values.project_flock_kandang_id + ? 'Pilih kandang terlebih dahulu' + : 'Pilih Produk' + } + isLoading={isLoadingStockProducts} + onMenuScrollToBottom={loadMoreStockProducts} + isError={ + isRepeaterInputError( + 'stocks', + 'product_warehouse_id', + idx + ).isError + } + errorMessage={ + isRepeaterInputError( + 'stocks', + 'product_warehouse_id', + idx + ).errorMessage + } + className={{ + wrapper: 'w-full min-w-48', + }} + isSearchable + isDisabled={ + type === 'detail' || + !formik.values.project_flock_kandang_id || + !recordingRestriction.canEditStock + } + isClearable={type !== 'detail'} + inputPrefix={ + stock.product_warehouse_id + ? getProductFlagBadgeAdornment( + stock.product_warehouse_id + ) + : undefined + } + /> + + +
+ + {getStockUsageAdornment(idx)} +
+ + {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( + +
+ +
+ + )} + + ))} + + +
+ {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( +
+ {selectedStocks.length > 0 && + recordingRestriction.canEditStock && ( + + )} + - )} - - - -
- )} -
+ +
+ )} + + )} {/* Transition Warning Banner -- MOVED UP -- */} {isTransitionPeriod && ( From 85f6677c2abf0262297ded6a873b912804cd142d Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 11 Mar 2026 15:35:37 +0700 Subject: [PATCH 16/21] fixing recording filter form --- .../recording/form/RecordingForm.tsx | 175 +++++++++++++++--- src/config/constant.ts | 6 + .../api/inventory/product-warehouse.d.ts | 10 + 3 files changed, 168 insertions(+), 23 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 9d90cdf2..f307b686 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -79,6 +79,7 @@ import { GROWING_RECORDING_APPROVAL_LINE, LAYING_RECORDING_APPROVAL_LINE, } from '@/config/approval-line'; +import { PROJECT_FLOCK_STATUS } from '@/config/constant'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { getRecordingRestriction } from '../recording-utils'; @@ -360,6 +361,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { loadMore: loadMoreProjectFlocks, } = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', { location_id: selectedProjectFlockLocationId, + status: PROJECT_FLOCK_STATUS.AKTIF, }); const projectFlockKandangLookupUrl = useMemo(() => { @@ -446,6 +448,23 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ? projectFlockKandangDetailData.data : undefined; + const selectedProjectFlockKandangId = useMemo(() => { + if (type === 'add') { + return projectFlockKandangLookup?.project_flock_kandang_id ?? null; + } + + return ( + projectFlockKandangDetail?.id ?? + initialValues?.project_flock?.project_flock_kandang_id ?? + null + ); + }, [ + type, + projectFlockKandangLookup, + projectFlockKandangDetail, + initialValues, + ]); + // ===== TRANSITION RESTRICTION LOGIC ===== const isTransitionPeriod = useMemo(() => { return ( @@ -756,8 +775,36 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return options; }, [locationOptions, projectFlockKandangDetail, type]); + const isProjectFlockActive = useCallback((projectFlock: ProjectFlock) => { + const approvalStepName = projectFlock.approval?.step_name + ?.trim() + .toLowerCase(); + if (approvalStepName) { + return approvalStepName === PROJECT_FLOCK_STATUS.AKTIF.toLowerCase(); + } + + return ( + projectFlock.status?.trim().toLowerCase() === + PROJECT_FLOCK_STATUS.AKTIF.toLowerCase() + ); + }, []); + + const activeProjectFlockIDs = useMemo(() => { + if (!isResponseSuccess(projectFlocksRawData)) return new Set(); + + const data = projectFlocksRawData.data as ProjectFlock[]; + return new Set( + data + .filter((projectFlock) => isProjectFlockActive(projectFlock)) + .map((projectFlock) => projectFlock.id) + ); + }, [projectFlocksRawData, isProjectFlockActive]); + const enhancedProjectFlockOptions = useMemo(() => { - const options = [...projectFlockOptions]; + const options = projectFlockOptions.filter((option) => { + if (type !== 'add') return true; + return activeProjectFlockIDs.has(Number(option.value)); + }); if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) { const currentProjectFlock = projectFlockKandangDetail.project_flock; @@ -773,7 +820,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } return options; - }, [projectFlockOptions, projectFlockKandangDetail, type]); + }, [ + projectFlockOptions, + projectFlockKandangDetail, + type, + activeProjectFlockIDs, + ]); const kandangOptions = useMemo(() => { let options: OptionType[] = []; @@ -881,8 +933,41 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { projectFlockKandangDetail, ]); + const isProductWarehouseBelongsToSelectedProjectFlockKandang = useCallback( + (productWarehouse: ProductWarehouse) => { + if (!selectedProjectFlockKandangId) return false; + + return ( + productWarehouse.project_flock_kandang?.id === + selectedProjectFlockKandangId + ); + }, + [selectedProjectFlockKandangId] + ); + + const scopedStockProductIds = useMemo(() => { + if (!isResponseSuccess(stockProducts) || !selectedProjectFlockKandangId) { + return new Set(); + } + + const data = stockProducts.data as unknown as ProductWarehouse[]; + return new Set( + data + .filter(isProductWarehouseBelongsToSelectedProjectFlockKandang) + .map((product) => product.id) + ); + }, [ + stockProducts, + selectedProjectFlockKandangId, + isProductWarehouseBelongsToSelectedProjectFlockKandang, + ]); + const unifiedStockProducts = useMemo(() => { - const options = [...stockProductOptions]; + const options = selectedProjectFlockKandangId + ? stockProductOptions.filter((option) => + scopedStockProductIds.has(Number(option.value)) + ) + : []; if ( initialValues && @@ -906,19 +991,30 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } return options; - }, [stockProductOptions, initialValues, type]); + }, [ + stockProductOptions, + initialValues, + type, + selectedProjectFlockKandangId, + scopedStockProductIds, + ]); const depletionProducts = useMemo(() => { const options: OptionType[] = []; - if (isResponseSuccess(depletionProductsData) && selectedKandang) { + if ( + isResponseSuccess(depletionProductsData) && + selectedProjectFlockKandangId + ) { const data = depletionProductsData.data as unknown as ProductWarehouse[]; - data.forEach((product) => { - options.push({ - value: product.id, - label: product.product.name, + data + .filter(isProductWarehouseBelongsToSelectedProjectFlockKandang) + .forEach((product) => { + options.push({ + value: product.id, + label: product.product.name, + }); }); - }); } if (initialValues && initialValues.depletions && type !== 'add') { @@ -941,19 +1037,27 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } return options; - }, [depletionProductsData, initialValues, type, selectedKandang]); + }, [ + depletionProductsData, + initialValues, + type, + selectedProjectFlockKandangId, + isProductWarehouseBelongsToSelectedProjectFlockKandang, + ]); const eggProducts = useMemo(() => { const options: OptionType[] = []; - if (isResponseSuccess(eggProductsData) && selectedKandang) { + if (isResponseSuccess(eggProductsData) && selectedProjectFlockKandangId) { const data = eggProductsData.data as unknown as ProductWarehouse[]; - data.forEach((product) => { - options.push({ - value: product.id, - label: product.product.name, + data + .filter(isProductWarehouseBelongsToSelectedProjectFlockKandang) + .forEach((product) => { + options.push({ + value: product.id, + label: product.product.name, + }); }); - }); } if (initialValues && initialValues.eggs && type !== 'add') { @@ -973,7 +1077,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } return options; - }, [eggProductsData, initialValues, type, selectedKandang]); + }, [ + eggProductsData, + initialValues, + type, + selectedProjectFlockKandangId, + isProductWarehouseBelongsToSelectedProjectFlockKandang, + ]); // ===== FORMIK SETUP ===== const formikInitialValues = useMemo(() => { @@ -2699,7 +2809,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='success' onClick={addStock} className='w-fit' - disabled={!recordingRestriction.canEditStock} + disabled={ + !formik.values.project_flock_kandang_id || + !recordingRestriction.canEditStock + } > Tambah Stok @@ -2841,7 +2954,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); }} options={getAvailableDepletionProductOptions(idx)} - placeholder='Pilih Kondisi' + placeholder={ + !formik.values.project_flock_kandang_id + ? 'Pilih kandang terlebih dahulu' + : 'Pilih Kondisi' + } isLoading={isLoadingDepletionProducts} onMenuScrollToBottom={loadMoreDepletionProducts} isError={ @@ -2860,6 +2977,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } isDisabled={ type === 'detail' || + !formik.values.project_flock_kandang_id || !recordingRestriction.canEditDepletion } className={{ @@ -2959,7 +3077,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='success' onClick={addDepletion} className='w-fit' - disabled={!recordingRestriction.canEditDepletion} + disabled={ + !formik.values.project_flock_kandang_id || + !recordingRestriction.canEditDepletion + } > Tambah Depletion @@ -3085,7 +3206,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); }} options={getAvailableEggProductOptions(idx)} - placeholder='Pilih Kondisi Telur' + placeholder={ + !formik.values.project_flock_kandang_id + ? 'Pilih kandang terlebih dahulu' + : 'Pilih Kondisi Telur' + } isLoading={isLoadingEggProducts} onMenuScrollToBottom={loadMoreEggProducts} isError={ @@ -3102,7 +3227,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { idx ).errorMessage } - isDisabled={type === 'detail'} + isDisabled={ + type === 'detail' || + !formik.values.project_flock_kandang_id + } className={{ wrapper: 'w-full min-w-48', }} @@ -3207,6 +3335,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='success' onClick={addEgg} className='w-fit' + disabled={!formik.values.project_flock_kandang_id} > Tambah Telur diff --git a/src/config/constant.ts b/src/config/constant.ts index ca0682b4..99594b65 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -555,6 +555,12 @@ export const APPROVAL_WORKFLOWS = { ], }; +export const PROJECT_FLOCK_STATUS = { + PENGAJUAN: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[0].step_name, + AKTIF: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[1].step_name, + SELESAI: APPROVAL_WORKFLOWS.PROJECT_FLOCKS[2].step_name, +} as const; + export const ACCEPTED_FILE_TYPE = { PDF: { 'application/pdf': ['.pdf'], diff --git a/src/types/api/inventory/product-warehouse.d.ts b/src/types/api/inventory/product-warehouse.d.ts index 060be2ab..4fc286c1 100644 --- a/src/types/api/inventory/product-warehouse.d.ts +++ b/src/types/api/inventory/product-warehouse.d.ts @@ -11,6 +11,16 @@ export type BaseProductWarehouse = { quantity: number; product: Product; warehouse: Warehouse; + project_flock_kandang?: { + id: number; + project_flock_id: number; + kandang_id: number; + period: number; + project_flock?: { + id: number; + flock_name: string; + }; + }; week?: number | null; }; From 6c7e310e6752a863015a4ae9626151b3cadddea6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 13 Mar 2026 13:33:18 +0700 Subject: [PATCH 17/21] feat(FE): Add support for available_qty in MovementForm --- .../inventory/movement/form/MovementForm.tsx | 56 ++++++++++++++++++- .../api/inventory/product-warehouse.d.ts | 1 + 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index f723e763..b44d98b3 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -82,6 +82,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { warehouse_id: number; warehouse_name: string; quantity: number; + available_qty?: number; } // ===== USE SELECT HOOKS ===== @@ -379,6 +380,8 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { warehouse_id: formik.values.source_warehouse_id ? formik.values.source_warehouse_id.toString() : '', + transfer_context: 'inventory_transfer', + stock_mode: 'exclude_chickin', } ); @@ -391,6 +394,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { warehouse_id: pw.warehouse.id, warehouse_name: pw.warehouse.name, quantity: pw.quantity, + available_qty: pw.available_qty, })) : []; }, [productWarehouses]); @@ -834,6 +838,18 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { }, [formik.values.products, formik.values.deliveries]); const getAvailableStock = useCallback( + (productId: number) => { + if (type === 'detail') return 0; + const productWarehouse = productWarehouseOptions.find( + (pw) => pw.product_id === productId + ); + + return productWarehouse?.available_qty ?? productWarehouse?.quantity ?? 0; + }, + [productWarehouseOptions, type] + ); + + const getTotalStock = useCallback( (productId: number) => { if (type === 'detail') return 0; const productWarehouse = productWarehouseOptions.find( @@ -844,6 +860,16 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { [productWarehouseOptions, type] ); + const hasAvailableQty = useCallback( + (productId: number) => { + const productWarehouse = productWarehouseOptions.find( + (pw) => pw.product_id === productId + ); + return productWarehouse?.available_qty !== undefined; + }, + [productWarehouseOptions] + ); + const getProductQtyBottomLabel = useCallback( (productIdx: number) => { if (type === 'detail') return undefined; @@ -851,16 +877,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { if (!product || !product.product_id) return undefined; const availableStock = getAvailableStock(product.product_id); + const totalStock = getTotalStock(product.product_id); const requestedQty = Number(product.product_qty) || 0; const remainingStock = availableStock - requestedQty; + const isAyamProduct = hasAvailableQty(product.product_id); if (requestedQty > 0) { + if (isAyamProduct) { + return `Sisa: ${formatNumber(remainingStock)} (Total: ${formatNumber(totalStock)})`; + } return `Sisa: ${formatNumber(remainingStock)}`; } + if (isAyamProduct) { + return `Tersedia: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)})`; + } + return `Tersedia: ${formatNumber(availableStock)}`; }, - [formik.values.products, getAvailableStock, type] + [ + formik.values.products, + getAvailableStock, + getTotalStock, + hasAvailableQty, + type, + ] ); const getDeliveryProductQtyBottomLabel = useCallback( @@ -922,15 +963,26 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { if (!product || !product.product_id) return null; const availableStock = getAvailableStock(product.product_id); + const totalStock = getTotalStock(product.product_id); const requestedQty = Number(product.product_qty) || 0; + const isAyamProduct = hasAvailableQty(product.product_id); if (requestedQty > availableStock) { + if (isAyamProduct) { + return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)} (Total: ${formatNumber(totalStock)}, terpakai untuk chickin: ${formatNumber(totalStock - availableStock)})`; + } return `Qty melebihi stok tersedia! Maksimal: ${formatNumber(availableStock)}`; } return null; }, - [formik.values.products, getAvailableStock, type] + [ + formik.values.products, + getAvailableStock, + getTotalStock, + hasAvailableQty, + type, + ] ); const validateDeliveryQty = useCallback( diff --git a/src/types/api/inventory/product-warehouse.d.ts b/src/types/api/inventory/product-warehouse.d.ts index 4fc286c1..a71e74a5 100644 --- a/src/types/api/inventory/product-warehouse.d.ts +++ b/src/types/api/inventory/product-warehouse.d.ts @@ -9,6 +9,7 @@ export type BaseProductWarehouse = { warehouse_id: number; uom: Uom; quantity: number; + available_qty?: number; product: Product; warehouse: Warehouse; project_flock_kandang?: { From 375de4c86c6b19c3866993c51863b5714ebfac33 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 16 Mar 2026 10:28:36 +0700 Subject: [PATCH 18/21] refactor(FE): Remove unused dependency from useEffect in RecordingForm --- src/components/pages/production/recording/form/RecordingForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index f307b686..c9236d19 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1771,7 +1771,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, [ projectFlockKandangDetail, type, - enhancedProjectFlockOptions, formik.values.project_flock_kandang_id, setFieldValue, ]); From b020f2b187df7b9ece4c340b282ecc114bf66292 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 16 Mar 2026 10:51:02 +0700 Subject: [PATCH 19/21] refactor(FE): Rename `available_qty` to `transfer_available_qty` --- .../pages/inventory/movement/form/MovementForm.tsx | 12 ++++++++---- src/types/api/inventory/product-warehouse.d.ts | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index b44d98b3..1907d498 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -82,7 +82,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { warehouse_id: number; warehouse_name: string; quantity: number; - available_qty?: number; + transfer_available_qty?: number; } // ===== USE SELECT HOOKS ===== @@ -394,7 +394,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { warehouse_id: pw.warehouse.id, warehouse_name: pw.warehouse.name, quantity: pw.quantity, - available_qty: pw.available_qty, + transfer_available_qty: pw.transfer_available_qty, })) : []; }, [productWarehouses]); @@ -844,7 +844,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { (pw) => pw.product_id === productId ); - return productWarehouse?.available_qty ?? productWarehouse?.quantity ?? 0; + return ( + productWarehouse?.transfer_available_qty ?? + productWarehouse?.quantity ?? + 0 + ); }, [productWarehouseOptions, type] ); @@ -865,7 +869,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { const productWarehouse = productWarehouseOptions.find( (pw) => pw.product_id === productId ); - return productWarehouse?.available_qty !== undefined; + return productWarehouse?.transfer_available_qty !== undefined; }, [productWarehouseOptions] ); diff --git a/src/types/api/inventory/product-warehouse.d.ts b/src/types/api/inventory/product-warehouse.d.ts index a71e74a5..726cc135 100644 --- a/src/types/api/inventory/product-warehouse.d.ts +++ b/src/types/api/inventory/product-warehouse.d.ts @@ -9,7 +9,7 @@ export type BaseProductWarehouse = { warehouse_id: number; uom: Uom; quantity: number; - available_qty?: number; + transfer_available_qty?: number; product: Product; warehouse: Warehouse; project_flock_kandang?: { From c4e27edd567f7b792c488fda073fb8131f33637e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 16 Mar 2026 11:01:29 +0700 Subject: [PATCH 20/21] feat(FE): Add delete functionality to Inventory and Movement tables --- .../adjustment/InventoryAdjustmentTable.tsx | 124 +++++++++++++++++- .../inventory/movement/MovementTable.tsx | 64 ++++++++- 2 files changed, 182 insertions(+), 6 deletions(-) diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index f8bd4443..fb0270ad 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -8,7 +8,7 @@ import { useState, } from 'react'; import { usePathname } from 'next/navigation'; -import useSWR from 'swr'; +import useSWR, { mutate } from 'swr'; import { Icon } from '@iconify/react'; import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table'; import { useFormik } from 'formik'; @@ -26,6 +26,10 @@ import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { WarehouseApi, ProductApi } from '@/services/api/master-data'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useUiStore } from '@/stores/ui/ui.store'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import toast from 'react-hot-toast'; import { InventoryAdjustment } from '@/types/api/inventory/adjustment'; import { Warehouse } from '@/types/api/master-data/warehouse'; import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant'; @@ -38,6 +42,62 @@ import { AdjustmentFilterType, } from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter'; import SelectInputRadio from '@/components/input/SelectInputRadio'; +import { CellContext } from '@tanstack/react-table'; + +const RowOptionsMenu = ({ + popoverPosition = 'bottom', + props, + deleteClickHandler, +}: { + popoverPosition: 'bottom' | 'top'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + const popoverId = `adjustment#${props.row.original.id}`; + const popoverAnchorName = `--anchor-adjustment#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + + return ( +
+ + + + + +
+ + + +
+
+
+ ); +}; const InventoryAdjustmentTable = () => { const { searchValue, setSearchValue, setTableState } = useUiStore(); @@ -182,12 +242,31 @@ const InventoryAdjustmentTable = () => { formik.validateForm(); }; - const { data: inventoryAdjustments, isLoading } = useSWR( + const { data: inventoryAdjustments, isLoading, mutate: refreshAdjustments } = useSWR( `${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`, InventoryAdjustmentApi.getAllFetcher ); + const singleDeleteHandler = async () => { + setIsDeleteLoading(true); + + const response = await InventoryAdjustmentApi.delete(selectedAdjustment?.id as number); + + singleDeleteModal.closeModal(); + setIsDeleteLoading(false); + + if (isResponseSuccess(response)) { + toast.success(response?.message || 'Successfully delete Adjustment!'); + refreshAdjustments(); + } else { + toast.error(response?.message || 'Failed to delete Adjustment'); + } + }; + const [sorting, setSorting] = useState([]); + const [selectedAdjustment, setSelectedAdjustment] = useState(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const singleDeleteModal = useModal(); useEffect(() => { updateFilter('search', searchValue); @@ -302,8 +381,32 @@ const InventoryAdjustmentTable = () => { header: 'Oleh', accessorFn: (row) => row.created_user?.name ?? '-', }, + { + id: 'actions', + header: 'Aksi', + cell: (props: CellContext) => { + const currentPageSize = props.table.getPaginationRowModel().rows.length; + const currentPageRows = props.table.getPaginationRowModel().flatRows; + const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; + + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + const deleteClickHandler = () => { + setSelectedAdjustment(props.row.original); + singleDeleteModal.openModal(); + }; + + return ( + + ); + }, + }, ], - [tableFilterState.pageSize, tableFilterState.page] + [tableFilterState.pageSize, tableFilterState.page, singleDeleteModal, setSelectedAdjustment] ); const updateSortingFilter = useCallback( @@ -532,6 +635,21 @@ const InventoryAdjustmentTable = () => {
+ + ); }; diff --git a/src/components/pages/inventory/movement/MovementTable.tsx b/src/components/pages/inventory/movement/MovementTable.tsx index f953099d..1dfbb7ce 100644 --- a/src/components/pages/inventory/movement/MovementTable.tsx +++ b/src/components/pages/inventory/movement/MovementTable.tsx @@ -8,7 +8,7 @@ import { useState, } from 'react'; import { usePathname } from 'next/navigation'; -import useSWR from 'swr'; +import useSWR, { mutate } from 'swr'; import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table'; import { useFormik } from 'formik'; @@ -21,6 +21,8 @@ import { cn } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useUiStore } from '@/stores/ui/ui.store'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import toast from 'react-hot-toast'; import Button from '@/components/Button'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import SelectInput, { useSelect } from '@/components/input/SelectInput'; @@ -41,9 +43,11 @@ import { const RowOptionsMenu = ({ popoverPosition = 'bottom', props, + deleteClickHandler, }: { popoverPosition: 'bottom' | 'top'; props: CellContext; + deleteClickHandler: () => void; }) => { const popoverId = `movement#${props.row.original.id}`; const popoverAnchorName = `--anchor-movement#${props.row.original.id}`; @@ -83,6 +87,20 @@ const RowOptionsMenu = ({ Detail + + +
@@ -206,12 +224,31 @@ const MovementTable = () => { }; const [sorting, setSorting] = useState([]); + const [selectedMovement, setSelectedMovement] = useState(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const singleDeleteModal = useModal(); - const { data: movements, isLoading } = useSWR( + const { data: movements, isLoading, mutate: refreshMovements } = useSWR( `${MovementApi.basePath}${getTableFilterQueryString()}`, MovementApi.getAllFetcher ); + const singleDeleteHandler = async () => { + setIsDeleteLoading(true); + + const response = await MovementApi.delete(selectedMovement?.id as number); + + singleDeleteModal.closeModal(); + setIsDeleteLoading(false); + + if (isResponseSuccess(response)) { + toast.success(response?.message || 'Successfully delete Movement!'); + refreshMovements(); + } else { + toast.error(response?.message || 'Failed to delete Movement'); + } + }; + useEffect(() => { updateFilter('search', searchValue); }, [searchValue, updateFilter]); @@ -275,16 +312,22 @@ const MovementTable = () => { const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + const deleteClickHandler = () => { + setSelectedMovement(props.row.original); + singleDeleteModal.openModal(); + }; + return ( ); }, }, ], - [tableFilterState.pageSize, tableFilterState.page] + [tableFilterState.pageSize, tableFilterState.page, singleDeleteModal, setSelectedMovement] ); return ( @@ -455,6 +498,21 @@ const MovementTable = () => { + + ); }; From c1087b37fb9acd53592210a23e95a841fc0f6dda Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 16 Mar 2026 11:02:25 +0700 Subject: [PATCH 21/21] chore(FE-prettier): Format code for better readability in inventory tables --- .../adjustment/InventoryAdjustmentTable.tsx | 27 ++++++++++++++----- .../inventory/movement/MovementTable.tsx | 17 +++++++++--- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index fb0270ad..ed34efc2 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -242,7 +242,11 @@ const InventoryAdjustmentTable = () => { formik.validateForm(); }; - const { data: inventoryAdjustments, isLoading, mutate: refreshAdjustments } = useSWR( + const { + data: inventoryAdjustments, + isLoading, + mutate: refreshAdjustments, + } = useSWR( `${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`, InventoryAdjustmentApi.getAllFetcher ); @@ -250,7 +254,9 @@ const InventoryAdjustmentTable = () => { const singleDeleteHandler = async () => { setIsDeleteLoading(true); - const response = await InventoryAdjustmentApi.delete(selectedAdjustment?.id as number); + const response = await InventoryAdjustmentApi.delete( + selectedAdjustment?.id as number + ); singleDeleteModal.closeModal(); setIsDeleteLoading(false); @@ -264,7 +270,9 @@ const InventoryAdjustmentTable = () => { }; const [sorting, setSorting] = useState([]); - const [selectedAdjustment, setSelectedAdjustment] = useState(undefined); + const [selectedAdjustment, setSelectedAdjustment] = useState< + InventoryAdjustment | undefined + >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const singleDeleteModal = useModal(); @@ -385,9 +393,11 @@ const InventoryAdjustmentTable = () => { id: 'actions', header: 'Aksi', cell: (props: CellContext) => { - const currentPageSize = props.table.getPaginationRowModel().rows.length; + const currentPageSize = + props.table.getPaginationRowModel().rows.length; const currentPageRows = props.table.getPaginationRowModel().flatRows; - const currentRowRelativeIndex = currentPageRows.findIndex((r) => r.id === props.row.id) + 1; + const currentRowRelativeIndex = + currentPageRows.findIndex((r) => r.id === props.row.id) + 1; const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; @@ -406,7 +416,12 @@ const InventoryAdjustmentTable = () => { }, }, ], - [tableFilterState.pageSize, tableFilterState.page, singleDeleteModal, setSelectedAdjustment] + [ + tableFilterState.pageSize, + tableFilterState.page, + singleDeleteModal, + setSelectedAdjustment, + ] ); const updateSortingFilter = useCallback( diff --git a/src/components/pages/inventory/movement/MovementTable.tsx b/src/components/pages/inventory/movement/MovementTable.tsx index 1dfbb7ce..2b6f11e6 100644 --- a/src/components/pages/inventory/movement/MovementTable.tsx +++ b/src/components/pages/inventory/movement/MovementTable.tsx @@ -224,11 +224,17 @@ const MovementTable = () => { }; const [sorting, setSorting] = useState([]); - const [selectedMovement, setSelectedMovement] = useState(undefined); + const [selectedMovement, setSelectedMovement] = useState< + Movement | undefined + >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const singleDeleteModal = useModal(); - const { data: movements, isLoading, mutate: refreshMovements } = useSWR( + const { + data: movements, + isLoading, + mutate: refreshMovements, + } = useSWR( `${MovementApi.basePath}${getTableFilterQueryString()}`, MovementApi.getAllFetcher ); @@ -327,7 +333,12 @@ const MovementTable = () => { }, }, ], - [tableFilterState.pageSize, tableFilterState.page, singleDeleteModal, setSelectedMovement] + [ + tableFilterState.pageSize, + tableFilterState.page, + singleDeleteModal, + setSelectedMovement, + ] ); return (