From cdf0442a2b28df322677d4e7dc7651beede39ab4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 9 Mar 2026 09:04:14 +0700 Subject: [PATCH 01/43] 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 698fe2e8516715f29288a1e7664e6b1a3d282b24 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 09:28:45 +0700 Subject: [PATCH 02/43] feat(FE): add pre-commit script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index edd750f6..34c07ec3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "start": "next start", "lint": "eslint", "prepare": "husky", - "format": "prettier --write ." + "format": "prettier --write .", + "pre-commit": "npm run format && npm run lint && npx tsc --noEmit && npm run build" }, "dependencies": { "@react-pdf/renderer": "^4.3.1", From d7e32f8f5bba34e9d92ce8a1ba2af803788573c4 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 09:29:22 +0700 Subject: [PATCH 03/43] feat(FE): create Daily Checklist Master Data Kandang page --- src/app/daily-checklist/master-data/kandang/page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/daily-checklist/master-data/kandang/page.tsx diff --git a/src/app/daily-checklist/master-data/kandang/page.tsx b/src/app/daily-checklist/master-data/kandang/page.tsx new file mode 100644 index 00000000..dd2b3142 --- /dev/null +++ b/src/app/daily-checklist/master-data/kandang/page.tsx @@ -0,0 +1,11 @@ +import { MasterKandangContent } from '@/figma-make/components/pages/master-data/kandang/MasterKandangContent'; + +const MasterKandangPage = () => { + return ( +
+ +
+ ); +}; + +export default MasterKandangPage; From c07b245eeb4713fed75c2225b7c4d5ada241b5ca Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 09:30:04 +0700 Subject: [PATCH 04/43] feat(FE): add Kandang submenu in Daily Checklist Master Data menu --- src/config/constant.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index 99c5ff9d..ca0682b4 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -20,6 +20,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ 'lti.daily_checklist.master_data.employee', 'lti.daily_checklist.master_data.activity', 'lti.daily_checklist.master_data.configuration', + 'lti.daily_checklist.master_data.kandang', ], submenu: [ { @@ -66,6 +67,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/daily-checklist/master-data/activity', permission: ['lti.daily_checklist.master_data.activity'], }, + { + text: 'Kandang', + link: '/daily-checklist/master-data/kandang', + permission: ['lti.daily_checklist.master_data.kandang'], + }, { text: 'Konfigurasi', link: '/daily-checklist/master-data/configuration', From b9a17f472bdbb938062b8daa306f2eb32b117285 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 09:30:26 +0700 Subject: [PATCH 05/43] feat: add daily checklist master data kandang permission in ROUTE_PERMISSIONS --- src/config/route-permission.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index dc638b29..8c65a611 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -21,6 +21,9 @@ export const ROUTE_PERMISSIONS: Record = { '/daily-checklist/master-data/configuration/': [ 'lti.daily_checklist.master_data.configuration', ], + '/daily-checklist/master-data/kandang/': [ + 'lti.daily_checklist.master_data.kandang', + ], // Production // Production - Project Flock From 0dbad23cd56e9ce40a83b925258e701b467c06cf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 09:31:05 +0700 Subject: [PATCH 06/43] feat(FE): implement options lazy loading by adding onLoadMore and isLoadingMore props --- .../components/base/multi-select.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/figma-make/components/base/multi-select.tsx b/src/figma-make/components/base/multi-select.tsx index 63ebdd36..656073c6 100644 --- a/src/figma-make/components/base/multi-select.tsx +++ b/src/figma-make/components/base/multi-select.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import { Check, ChevronsUpDown, X } from 'lucide-react'; +import { Check, ChevronsUpDown, X, Loader2 } from 'lucide-react'; import { cn } from '@/lib/helper'; import { Button } from '@/figma-make/components/base/button'; import { @@ -29,6 +29,8 @@ interface MultiSelectProps { selected: string[]; onChange: (selected: string[]) => void; onSearchChange?: (value: string) => void; + onLoadMore?: () => void; + isLoadingMore?: boolean; placeholder?: string; className?: string; disabled?: boolean; @@ -39,6 +41,8 @@ export function MultiSelect({ selected, onChange, onSearchChange, + onLoadMore, + isLoadingMore, placeholder = 'Select items...', className, disabled, @@ -115,7 +119,18 @@ export function MultiSelect({ onValueChange={onSearchChange} /> No item found. - + { + const target = e.currentTarget; + if ( + target.scrollHeight - target.scrollTop <= + target.clientHeight + 1 + ) { + onLoadMore?.(); + } + }} + > {options.map((option) => ( ))} + {isLoadingMore && ( +
+ +
+ )}
From 2b096099d34b4c78cad39ec046f9d9dc3ee1af8f Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 12:28:29 +0700 Subject: [PATCH 07/43] feat: add kandang group column --- src/components/pages/master-data/kandang/KandangsTable.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/pages/master-data/kandang/KandangsTable.tsx b/src/components/pages/master-data/kandang/KandangsTable.tsx index 9d923cbe..698d3a96 100644 --- a/src/components/pages/master-data/kandang/KandangsTable.tsx +++ b/src/components/pages/master-data/kandang/KandangsTable.tsx @@ -314,6 +314,10 @@ const KandangsTable = () => { accessorFn: (row) => row.pic?.name ?? '-', header: 'PIC', }, + { + accessorFn: (row) => row.kandang_group?.name ?? '-', + header: 'Kandang Group', + }, { header: 'Aksi', cell: (props: CellContext) => { From e1856926ea505195eb24951a17a71cd7acd51598 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 12:28:48 +0700 Subject: [PATCH 08/43] feat(FE): add group to kandang form schema --- .../pages/master-data/kandang/form/KandangForm.schema.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/pages/master-data/kandang/form/KandangForm.schema.ts b/src/components/pages/master-data/kandang/form/KandangForm.schema.ts index 3da93aef..39f9a632 100644 --- a/src/components/pages/master-data/kandang/form/KandangForm.schema.ts +++ b/src/components/pages/master-data/kandang/form/KandangForm.schema.ts @@ -1,3 +1,4 @@ +import { OptionType } from '@/components/input/SelectInput'; import * as Yup from 'yup'; type KandangFormSchemaType = { @@ -19,6 +20,7 @@ type KandangFormSchemaType = { } | undefined | null; + group?: OptionType; }; export const KandangFormSchema: Yup.ObjectSchema = @@ -42,6 +44,11 @@ export const KandangFormSchema: Yup.ObjectSchema = value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), + + group: Yup.object({ + value: Yup.number().min(1).required('Kandang Grup wajib diisi!'), + label: Yup.string().required('Kandang Grup wajib diisi!'), + }).required('Kandang Grup wajib diisi!'), }); export const UpdateKandangFormSchema = KandangFormSchema; From 1b3e5f94f1c66b021931d45f9ae97f11b3f3b2c3 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 12:29:04 +0700 Subject: [PATCH 09/43] feat(FE): add Kandang Group input --- .../master-data/kandang/form/KandangForm.tsx | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/components/pages/master-data/kandang/form/KandangForm.tsx b/src/components/pages/master-data/kandang/form/KandangForm.tsx index 87ddfd70..be6677d5 100644 --- a/src/components/pages/master-data/kandang/form/KandangForm.tsx +++ b/src/components/pages/master-data/kandang/form/KandangForm.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { useFormik } from 'formik'; +import { getIn, useFormik } from 'formik'; import { toast } from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -34,6 +34,8 @@ import NumberInput from '@/components/input/NumberInput'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { User } from '@/types/api/api-general'; +import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang'; +import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang'; interface KandangFormProps { type?: 'add' | 'edit' | 'detail'; @@ -96,6 +98,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { label: initialValues.pic.name, } : null, + group: initialValues?.kandang_group + ? { + value: initialValues.kandang_group.id, + label: initialValues.kandang_group.name, + } + : undefined, }; }, [initialValues]); @@ -111,6 +119,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { location_id: values.locationId!, capacity: values.capacity ? parseInt(values.capacity.toString()) : 0, pic_id: values.picId!, + group_id: values.group?.value as number, }; switch (type) { @@ -162,6 +171,23 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { formik.setFieldValue('picId', (val as OptionType)?.value); }; + // Kandang Group + const { + setInputValue: setKandangGroupSelectInputValue, + options: kandangGroupOptions, + isLoadingOptions: isLoadingKandangGroupOptions, + loadMore: loadMoreKandangGroups, + } = useSelect( + DailyChecklistKandangApi.basePath, + 'id', + 'name' + ); + + const kandangGroupChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldTouched('group', true); + formik.setFieldValue('group', val); + }; + const deleteKandangClickHandler = () => { deleteModal.openModal(); }; @@ -184,6 +210,11 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { // ===== Formik Error List ===== const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik); + console.log({ + values: formik.values, + errors: formik.errors, + }); + return ( <>
@@ -269,6 +300,24 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => { isDisabled={type === 'detail'} isClearable /> + +
From fb9e863862a9697086b3547183e2362bbad0d4f5 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 12:29:20 +0700 Subject: [PATCH 10/43] feat(FE): create MasterKandangContent component --- .../kandang/MasterKandangContent.tsx | 679 ++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 src/figma-make/components/pages/master-data/kandang/MasterKandangContent.tsx diff --git a/src/figma-make/components/pages/master-data/kandang/MasterKandangContent.tsx b/src/figma-make/components/pages/master-data/kandang/MasterKandangContent.tsx new file mode 100644 index 00000000..831fdacc --- /dev/null +++ b/src/figma-make/components/pages/master-data/kandang/MasterKandangContent.tsx @@ -0,0 +1,679 @@ +'use client'; + +import { useState } from 'react'; +import { Plus, MoreVertical, Pencil, Trash2, Search } from 'lucide-react'; +import { Card, CardContent } from '@/figma-make/components/base/card'; +import { Button } from '@/figma-make/components/base/button'; +import { Label } from '@/figma-make/components/base/label'; +import { Input } from '@/figma-make/components/base/input'; +import { Badge } from '@/figma-make/components/base/badge'; +import { MultiSelect } from '@/figma-make/components/base/multi-select'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/figma-make/components/base/select'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/figma-make/components/base/dialog'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/figma-make/components/base/alert-dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/figma-make/components/base/dropdown-menu'; +import { toast } from 'sonner'; +import useSWR from 'swr'; +import { DailyChecklistKandangApi } from '@/services/api/daily-checklist/kandang'; +import Table from '@/components/Table'; +import { DailyChecklistKandang } from '@/types/api/daily-checklist/kandang'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { cn } from '@/lib/helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ColumnDef } from '@tanstack/react-table'; +import { useSelect } from '@/components/input/SelectInput'; +import { KandangApi, LocationApi } from '@/services/api/master-data'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang'; +import { UserApi } from '@/services/api/user'; + +export function MasterKandangContent() { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + location_id: '', + status: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + search: 'search', + location_id: 'location_id', + }, + }); + + const { + data: dailyChecklistKandangs, + isLoading: isLoadingDailyChecklistKandangs, + mutate: refreshDailyChecklistKandangs, + } = useSWR( + `${DailyChecklistKandangApi.basePath}${getTableFilterQueryString()}`, + DailyChecklistKandangApi.getAllFetcher, + { + keepPreviousData: true, + } + ); + const { options: locationOptions } = useSelect( + LocationApi.basePath, + 'id', + 'name', + 'search', + { + page: '1', + limit: '100', + } + ); + + const { options: picOptions } = useSelect( + UserApi.basePath, + 'id', + 'name', + 'search', + { + page: '1', + limit: '100', + } + ); + + const { + options: kandangOptions, + isLoadingMore: isLoadingKandangOptionsMore, + loadMore: loadMoreKandang, + } = useSelect(KandangApi.basePath, 'id', 'name'); + + const [showModal, setShowModal] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [kandangToDelete, setKandangToDelete] = useState(null); + const [loading, setLoading] = useState(false); + const [modalMode, setModalMode] = useState<'create' | 'edit'>('create'); + const [kandangForm, setKandangForm] = useState({ + id: 0, + name: '', + location_id: 0, + pic_id: 0, + // recording_kandangs: [] as number[], + }); + + const dailyChecklistKandangColumns: ColumnDef[] = [ + { + id: 'name', + header: 'Nama', + accessorKey: 'name', + enableSorting: false, + }, + { + id: 'location', + header: 'Lokasi', + accessorKey: 'location', + enableSorting: false, + cell: ({ row }) => row.original.location.name ?? '-', + }, + { + id: 'pic', + header: 'PIC', + accessorKey: 'pic', + enableSorting: false, + cell: ({ row }) => row.original.pic.name ?? '-', + }, + { + id: 'recording_kandangs', + header: 'Kandang Recording', + accessorKey: 'recording_kandangs', + enableSorting: false, + cell: ({ row }) => + row.original.recording_kandangs?.length > 0 + ? row.original.recording_kandangs.map((item) => item.name).join(', ') + : '-', + }, + { + id: 'action', + header: 'Aksi', + accessorKey: 'action', + enableSorting: false, + cell: ({ row }) => ( + + + + + + handleEdit(row.original)}> + + Edit + + handleDeleteClick(row.original.id)} + className='text-red-600' + > + + Hapus + + + + ), + }, + ]; + + const handleAdd = () => { + setModalMode('create'); + setKandangForm({ + id: 0, + name: '', + location_id: 0, + pic_id: 0, + // recording_kandangs: [] + }); + setShowModal(true); + }; + + const handleEdit = (dailyChecklistKandang: DailyChecklistKandang) => { + setModalMode('edit'); + setKandangForm({ + id: dailyChecklistKandang.id, + name: dailyChecklistKandang.name, + location_id: dailyChecklistKandang.location.id, + pic_id: dailyChecklistKandang.pic.id, + // recording_kandangs: + // dailyChecklistKandang.recording_kandangs.map((item) => item.id) ?? [], + }); + setShowModal(true); + }; + + const handleSave = async () => { + if (!kandangForm.name.trim()) { + toast.error('Nama harus diisi'); + return; + } + + if (!kandangForm.location_id) { + toast.error('Lokasi wajib diisi'); + return; + } + + // if (!kandangForm.recording_kandangs.length) { + // toast.error('Kandang recording wajib diisi'); + // return; + // } + + setLoading(true); + + try { + if (modalMode === 'create') { + const createDailyChecklistKandangResponse = + await DailyChecklistKandangApi.create({ + name: kandangForm.name.trim(), + location_id: kandangForm.location_id, + pic_id: kandangForm.pic_id, + // recording_kandang_ids: kandangForm.recording_kandangs, + }); + + if (isResponseError(createDailyChecklistKandangResponse)) { + console.error( + 'Error creating kandang:', + createDailyChecklistKandangResponse.message + ); + toast.error('Gagal menambahkan kandang'); + return; + } + + refreshDailyChecklistKandangs(); + toast.success('Kandang berhasil ditambahkan'); + } else { + const updateDailyChecklistKandangResponse = + await DailyChecklistKandangApi.update(kandangForm.id, { + name: kandangForm.name.trim(), + location_id: kandangForm.location_id, + pic_id: kandangForm.pic_id, + // recording_kandang_ids: kandangForm.recording_kandangs, + }); + + if (isResponseError(updateDailyChecklistKandangResponse)) { + console.error( + 'Error updating kandang:', + updateDailyChecklistKandangResponse.message + ); + toast.error('Gagal menambahkan Kandang'); + return; + } + + refreshDailyChecklistKandangs(); + toast.success('Kandang berhasil diubah'); + } + + setShowModal(false); + setKandangForm({ + id: 0, + name: '', + location_id: 0, + pic_id: 0, + // recording_kandangs: [], + }); + } catch (error) { + console.error('Error saving kandang:', error); + toast.error('Terjadi kesalahan saat menyimpan kandang'); + } finally { + setLoading(false); + } + }; + + const handleDeleteClick = (kandangId: number) => { + setKandangToDelete(kandangId); + setShowDeleteConfirm(true); + }; + + const handleConfirmDelete = async () => { + if (!kandangToDelete) return; + + setLoading(true); + + try { + const deleteKandangResponse = + await DailyChecklistKandangApi.delete(kandangToDelete); + + if (isResponseError(deleteKandangResponse)) { + console.error('Error deleting kandang:', deleteKandangResponse.message); + toast.error('Gagal menghapus kandang'); + return; + } + + refreshDailyChecklistKandangs(); + toast.success('Kandang berhasil dihapus'); + setShowDeleteConfirm(false); + setKandangToDelete(null); + } catch (error) { + console.error('Error deleting kandang:', error); + toast.error('Terjadi kesalahan saat menghapus kandang'); + } finally { + setLoading(false); + } + }; + + if (isLoadingDailyChecklistKandangs && !dailyChecklistKandangs) { + return ( +
+
+
+

+ Master Kandang +

+

+ Master Data • Kandang +

+
+ + + Memuat data... + + +
+
+ ); + } + + return ( +
+
+ {/* Page Title */} +
+

+ Master Kandang +

+

+ Master Data • Kandang +

+
+ + {/* Main Card */} + + + {/* Single Toolbar Row */} +
+ {/* LEFT: Search + Filters */} +
+
+ + + updateFilter('search', e.target.value)} + className={{ + wrapper: 'w-full sm:w-[280px] border-gray-200', + inputWrapper: 'px-3 py-2 h-fit rounded-md', + input: 'text-sm', + }} + startAdornment={ + + } + /> +
+ + + + {/* */} +
+ + {/* RIGHT: Export + Add */} +
+ +
+
+ + {/* Table */} + + data={ + isResponseSuccess(dailyChecklistKandangs) + ? dailyChecklistKandangs?.data + : [] + } + columns={dailyChecklistKandangColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(dailyChecklistKandangs) + ? dailyChecklistKandangs?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(dailyChecklistKandangs) + ? dailyChecklistKandangs?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingDailyChecklistKandangs} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(dailyChecklistKandangs) && + dailyChecklistKandangs?.data?.length === 0, + }), + tableWrapperClassName: + 'overflow-x-auto border border-solid border-base-content/10 rounded-none', + headerRowClassName: 'bg-gray-50/50', + headerColumnClassName: + 'text-left py-3.5 px-6 text-sm font-semibold text-gray-700', + paginationClassName: 'px-4', + }} + /> +
+
+
+ + {/* Add/Edit Modal */} + + + + + {modalMode === 'create' ? 'Tambah Kandang' : 'Edit Kandang'} + + + {modalMode === 'create' + ? 'Masukkan detail Kandang baru' + : 'Ubah detail Kandang'} + + +
+
+ + + setKandangForm({ ...kandangForm, name: e.target.value }) + } + placeholder='Masukkan nama Kandang' + className='mt-1.5' + disabled={loading} + /> +
+
+ + +
+ +
+ + +
+ + {/*
+ + ({ + value: String(k.value), + label: k.label, + }))} + selected={kandangForm.recording_kandangs.map((id) => + String(id) + )} + onChange={(selected) => + setKandangForm({ + ...kandangForm, + recording_kandangs: selected.map((id) => Number(id)), + }) + } + placeholder='Pilih kandang' + isLoadingMore={isLoadingKandangOptionsMore} + onLoadMore={loadMoreKandang} + className='mt-1.5' + /> +
*/} + + {/*
+ + ({ + value: String(k.value), + label: k.label, + }))} + selected={kandangForm.kandang_ids.map((id) => String(id))} + onChange={(selected) => + setKandangForm({ + ...kandangForm, + kandang_ids: selected.map((id) => Number(id)), + }) + } + placeholder='Pilih kandang' + className='mt-1.5' + /> +
*/} + {/*
+ + +
*/} +
+ + + + +
+
+ + {/* Delete Confirmation */} + + + + Hapus Kandang? + + Data Kandang akan dihapus secara permanen. + + + + Batal + + {loading ? 'Menghapus...' : 'Hapus'} + + + + +
+ ); +} From 29e33560f852b8ecbaf2f8fc16f4766312843ee3 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 12:29:36 +0700 Subject: [PATCH 11/43] feat(FE): create daily checklist kandang API service --- src/services/api/daily-checklist/kandang.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/services/api/daily-checklist/kandang.ts diff --git a/src/services/api/daily-checklist/kandang.ts b/src/services/api/daily-checklist/kandang.ts new file mode 100644 index 00000000..e92ccd50 --- /dev/null +++ b/src/services/api/daily-checklist/kandang.ts @@ -0,0 +1,20 @@ +import { BaseApiService } from '@/services/api/base'; +import { + DailyChecklistKandang, + CreateDailyChecklistKandangPayload, + UpdateDailyChecklistKandangPayload, +} from '@/types/api/daily-checklist/kandang'; + +export class DailyChecklistKandangApiService extends BaseApiService< + DailyChecklistKandang, + CreateDailyChecklistKandangPayload, + UpdateDailyChecklistKandangPayload +> { + constructor(basePath: string = '/master-data/kandang-groups') { + super(basePath); + } +} + +export const DailyChecklistKandangApi = new DailyChecklistKandangApiService( + '/master-data/kandang-groups' +); From f1a4d9b648baf5bad22939fa8d72f30bb4e7a8f1 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 12:29:52 +0700 Subject: [PATCH 12/43] feat(FE): create daily checklist kandang types --- src/types/api/daily-checklist/kandang.d.ts | 24 ++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/types/api/daily-checklist/kandang.d.ts diff --git a/src/types/api/daily-checklist/kandang.d.ts b/src/types/api/daily-checklist/kandang.d.ts new file mode 100644 index 00000000..a67344b0 --- /dev/null +++ b/src/types/api/daily-checklist/kandang.d.ts @@ -0,0 +1,24 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { BaseKandang } from '@/types/api/master-data/kandang'; +import { BaseLocation } from '@/types/api/master-data/location'; +import { BaseUser } from '@/types/api/user'; + +export type BaseDailyChecklistKandang = { + id: number; + name: string; + location: BaseLocation; + recording_kandangs: Pick[]; + pic: BaseUser; +}; + +export type DailyChecklistKandang = BaseMetadata & BaseDailyChecklistKandang; + +export type CreateDailyChecklistKandangPayload = { + name: string; + location_id: number; + pic_id: number; + // recording_kandang_ids: number[]; +}; + +export type UpdateDailyChecklistKandangPayload = + CreateDailyChecklistKandangPayload; From 918c51e83bde4a80244907b5b0c47ac0c736ee59 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 12:30:16 +0700 Subject: [PATCH 13/43] fix(FE): add kandang_group to BaseKandang and add group_id to CreateKandangPayload --- src/types/api/master-data/kandang.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/types/api/master-data/kandang.d.ts b/src/types/api/master-data/kandang.d.ts index eafa0334..032f67a4 100644 --- a/src/types/api/master-data/kandang.d.ts +++ b/src/types/api/master-data/kandang.d.ts @@ -1,6 +1,7 @@ import { BaseMetadata } from '@/types/api/api-general'; import { BaseLocation } from '@/types/api/master-data/location'; import { BaseUser } from '@/types/api/user'; +import { BaseDailyChecklistKandang } from '@/types/api/daily-checklist/kandang'; export type BaseKandang = { id: number; @@ -10,6 +11,7 @@ export type BaseKandang = { capacity: number; pic: BaseUser; project_flock_kandang_id?: number; + kandang_group: Pick; }; export type Kandang = BaseMetadata & BaseKandang; @@ -19,6 +21,7 @@ export type CreateKandangPayload = { location_id: number; capacity: number; pic_id: number; + group_id: number; }; export type UpdateKandangPayload = CreateKandangPayload; From f83abc91da745918799903183b154b9f10118b16 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 9 Mar 2026 12:30:49 +0700 Subject: [PATCH 14/43] chore(FE): remove unncessary code --- .../kandang/MasterKandangContent.tsx | 94 ------------------- 1 file changed, 94 deletions(-) diff --git a/src/figma-make/components/pages/master-data/kandang/MasterKandangContent.tsx b/src/figma-make/components/pages/master-data/kandang/MasterKandangContent.tsx index 831fdacc..3d3ba1c2 100644 --- a/src/figma-make/components/pages/master-data/kandang/MasterKandangContent.tsx +++ b/src/figma-make/components/pages/master-data/kandang/MasterKandangContent.tsx @@ -408,22 +408,6 @@ export function MasterKandangContent() { ))} - - {/* */}
{/* RIGHT: Export + Add */} @@ -555,84 +539,6 @@ export function MasterKandangContent() { - - {/*
- - ({ - value: String(k.value), - label: k.label, - }))} - selected={kandangForm.recording_kandangs.map((id) => - String(id) - )} - onChange={(selected) => - setKandangForm({ - ...kandangForm, - recording_kandangs: selected.map((id) => Number(id)), - }) - } - placeholder='Pilih kandang' - isLoadingMore={isLoadingKandangOptionsMore} - onLoadMore={loadMoreKandang} - className='mt-1.5' - /> -
*/} - - {/*
- - ({ - value: String(k.value), - label: k.label, - }))} - selected={kandangForm.kandang_ids.map((id) => String(id))} - onChange={(selected) => - setKandangForm({ - ...kandangForm, - kandang_ids: selected.map((id) => Number(id)), - }) - } - placeholder='Pilih kandang' - className='mt-1.5' - /> -
*/} - {/*
- - -
*/} )} - {!isApproved && !isRejected && ( + {!restrictionInfo.isLocked && !isApproved && !isRejected && ( )} - {!isApproved && !isRejected && ( + {!restrictionInfo.isLocked && !isApproved && !isRejected && ( )} - {!restrictionInfo.isLocked && !isApproved && !isRejected && ( + {!isApproved && !isRejected && ( )} - {!restrictionInfo.isLocked && !isApproved && !isRejected && ( + {!isApproved && !isRejected && ( + + + )} + + ))} + + + + {(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 35/43] 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 36/43] 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 85dee607e08da804e3a974c5c07922d77e881d27 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Sat, 14 Mar 2026 15:42:01 +0700 Subject: [PATCH 37/43] fix: get kandang options from project flock kandang instead of kandang master data --- .../form/InventoryAdjustmentForm.tsx | 73 ++++--------------- 1 file changed, 15 insertions(+), 58 deletions(-) diff --git a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx index ff710329..1abeab92 100644 --- a/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx +++ b/src/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.tsx @@ -15,7 +15,7 @@ import { InventoryAdjustmentFormSchema, InventoryAdjustmentFormValues, } from '@/components/pages/inventory/adjustment/form/InventoryAdjustmentForm.schema'; -import { KandangApi, LocationApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; import { ProjectFlockApi, ProjectFlockKandangApi, @@ -32,8 +32,6 @@ import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { Location } from '@/types/api/master-data/location'; import { ProjectFlock } from '@/types/api/production/project-flock'; -import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; -import { Kandang } from '@/types/api/master-data/kandang'; import { Product } from '@/types/api/master-data/product'; import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; import { BaseApiResponse } from '@/types/api/api-general'; @@ -119,40 +117,19 @@ const InventoryAdjustmentForm = ({ } ); - const { rawData: approvedProjectFlockKandangsRawData } = - useSelect( - ProjectFlockKandangApi.basePath, - 'id', - 'id', - 'search', - { - step_name: 'Disetujui', - limit: '100', - } - ); - - const approvedProjectFlockKandangs = useMemo(() => { - if ( - approvedProjectFlockKandangsRawData && - 'data' in approvedProjectFlockKandangsRawData - ) { - return approvedProjectFlockKandangsRawData.data as ProjectFlockKandang[]; - } - return []; - }, [approvedProjectFlockKandangsRawData]); - const { - setInputValue: setKandangInputValue, - options: kandangOptionsFromApi, - isLoadingOptions: isLoadingKandangOptions, - loadMore: loadMoreKandangs, - } = useSelect( - selectedProjectFlock ? KandangApi.basePath : '', - 'id', - 'name', + options: projectFlockKandangOptions, + loadMore: loadMoreProjectFlockKandangs, + setInputValue: setProjectFlockKandangInputValue, + isLoadingOptions: isLoadingProjectFlockKandangOptions, + } = useSelect( + selectedProjectFlock ? ProjectFlockKandangApi.basePath : '', + 'kandang.id', + 'kandang.name', 'search', { - location_id: selectedProjectFlockLocationId, + step_name: 'Disetujui', + project_flock_id: String(selectedProjectFlock?.value), } ); @@ -222,26 +199,6 @@ const InventoryAdjustmentForm = ({ return (product?.flags as string[]) || []; }, [selectedProduct, productOptions]); - const kandangOptions = useMemo(() => { - let options: OptionType[] = []; - - if (selectedProjectFlock) { - const approvedKandangIds = approvedProjectFlockKandangs - .filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value) - .map((pfk) => pfk.kandang_id); - - options = kandangOptionsFromApi.filter((kandang) => - approvedKandangIds.includes(kandang.value as number) - ); - } - - return options; - }, [ - selectedProjectFlock, - kandangOptionsFromApi, - approvedProjectFlockKandangs, - ]); - const formikInitialValues = useMemo>( () => ({ location: null, @@ -693,10 +650,10 @@ const InventoryAdjustmentForm = ({ label='Kandang' value={selectedKandang} onChange={kandangChangeHandler} - onInputChange={setKandangInputValue} - options={kandangOptions} - onMenuScrollToBottom={loadMoreKandangs} - isLoading={isLoadingKandangOptions} + onInputChange={setProjectFlockKandangInputValue} + options={projectFlockKandangOptions} + onMenuScrollToBottom={loadMoreProjectFlockKandangs} + isLoading={isLoadingProjectFlockKandangOptions} isError={ formik.touched.kandang_id && Boolean(formik.errors.kandang_id) } From 0c8a833e008c88ec1a32753929f581db1fbb2c89 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 16 Mar 2026 09:41:01 +0700 Subject: [PATCH 38/43] fix: add generic for OptionType type --- src/components/input/SelectInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 0edb3b6d..32f8dbcd 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -24,8 +24,8 @@ import { } from '@/types/api/api-general'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -export interface OptionType { - value: string | number; +export interface OptionType { + value: T; label: string; className?: string; labelClassName?: string; From 0f7a2bd796ea01404722f43438074dd9628d1ff8 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 16 Mar 2026 09:41:23 +0700 Subject: [PATCH 39/43] fix: adjust formik schema for warehouse --- .../adjustment/InventoryAdjustmentTable.tsx | 28 ++++++------------- .../adjustment/filter/AdjustmentFilter.ts | 3 +- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index 90b68b7d..f8bd4443 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -80,13 +80,13 @@ const InventoryAdjustmentTable = () => { const formik = useFormik({ initialValues: { product_id: null, - warehouse_id: null, + warehouse: null, transaction_type: null, }, validationSchema: AdjustmentFilterSchema, onSubmit: (values, { setSubmitting }) => { updateFilter('productFilter', values.product_id || ''); - updateFilter('warehouseFilter', values.warehouse_id || ''); + updateFilter('warehouseFilter', String(values.warehouse?.value) || ''); updateFilter('transactionTypeFilter', values.transaction_type || ''); filterModal.closeModal(); setSubmitting(false); @@ -142,14 +142,11 @@ const InventoryAdjustmentTable = () => { [formik] ); - const handleFilterWarehouseChange = useCallback( - (val: OptionType | OptionType[] | null) => { - const warehouse = val as OptionType | null; - const warehouseId = warehouse?.value ? String(warehouse.value) : null; - formik.setFieldValue('warehouse_id', warehouseId); - }, - [formik] - ); + const handleFilterWarehouseChange = ( + val: OptionType | OptionType[] | null + ) => { + formik.setFieldValue('warehouse', val); + }; const handleFilterTransactionTypeChange = useCallback( (val: OptionType | OptionType[] | null) => { @@ -170,15 +167,6 @@ const InventoryAdjustmentTable = () => { ); }, [formik.values.product_id, productOptions]); - const warehouseIdValue = useMemo(() => { - if (!formik.values.warehouse_id) return null; - return ( - warehouseOptions.find( - (opt) => String(opt.value) === formik.values.warehouse_id - ) || null - ); - }, [formik.values.warehouse_id, warehouseOptions]); - const transactionTypeValue = useMemo(() => { if (!formik.values.transaction_type) return null; return ( @@ -502,7 +490,7 @@ const InventoryAdjustmentTable = () => { label='Gudang' placeholder='Pilih Gudang' options={warehouseOptions} - value={warehouseIdValue} + value={formik.values.warehouse} onChange={handleFilterWarehouseChange} onInputChange={setWarehouseInputValue} isLoading={isLoadingWarehouseOptions} diff --git a/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts b/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts index 4568618f..e4015e07 100644 --- a/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts +++ b/src/components/pages/inventory/adjustment/filter/AdjustmentFilter.ts @@ -1,4 +1,5 @@ import { string, object } from 'yup'; +import { OptionType } from '@/components/input/SelectInput'; export const AdjustmentFilterSchema = object().shape({ product_id: string().nullable(), @@ -8,6 +9,6 @@ export const AdjustmentFilterSchema = object().shape({ export type AdjustmentFilterType = { product_id: string | null; - warehouse_id: string | null; transaction_type: string | null; + warehouse: OptionType | null; }; From 375de4c86c6b19c3866993c51863b5714ebfac33 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 16 Mar 2026 10:28:36 +0700 Subject: [PATCH 40/43] 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 41/43] 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 42/43] 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 43/43] 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 (