From 8d92da75cfd858650b06db8a34b71b1144a80936 Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Wed, 1 Apr 2026 10:14:05 +0700 Subject: [PATCH 01/21] codex: initiated changes --- .husky/pre-commit | 2 +- package.json | 3 +- .../pages/closing/table/SalesClosingTable.tsx | 2 +- .../table/sapronak/OutgoingSapronaksTable.tsx | 4 +- .../pages/marketing/MarketingTable.tsx | 2 +- .../pages/marketing/SalesOrderFormModal.tsx | 1 - .../marketing/form/MarketingForm.schema.ts | 29 ++- .../delivery-order/DeliverOrderProduct.tsx | 10 +- .../sales-order/SalesOrderProduct.schema.ts | 26 ++- .../sales-order/SalesOrderProductForm.tsx | 118 ++++++++--- .../table-view/DeliveryOrderProductTable.tsx | 5 +- .../table-view/SalesOrderProductTable.tsx | 6 +- .../recording/form/RecordingForm.schema.ts | 6 + .../recording/form/RecordingForm.tsx | 199 ++++++------------ .../export/DailyMarketingExportPDF.tsx | 2 +- .../export/DailyMarketingExportXLSX.tsx | 4 +- .../marketing/tab/DailyMarketingTab.tsx | 6 +- src/lib/product-warehouse.ts | 57 +++++ src/types/api/marketing/marketing.d.ts | 7 +- src/types/api/production/recording.d.ts | 2 + 20 files changed, 287 insertions(+), 204 deletions(-) create mode 100644 src/lib/product-warehouse.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index ff51d55a..ac8a41c7 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,3 @@ npm run format npm run lint -npx tsc --noEmit \ No newline at end of file +npm run typecheck diff --git a/package.json b/package.json index 34c07ec3..90d941ce 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "build": "next build --turbopack", "start": "next start", "lint": "eslint", + "typecheck": "next typegen && tsc --noEmit", "prepare": "husky", "format": "prettier --write .", - "pre-commit": "npm run format && npm run lint && npx tsc --noEmit && npm run build" + "pre-commit": "npm run format && npm run lint && npm run typecheck && npm run build" }, "dependencies": { "@react-pdf/renderer": "^4.3.1", diff --git a/src/components/pages/closing/table/SalesClosingTable.tsx b/src/components/pages/closing/table/SalesClosingTable.tsx index e362f1e0..2e3e7fdf 100644 --- a/src/components/pages/closing/table/SalesClosingTable.tsx +++ b/src/components/pages/closing/table/SalesClosingTable.tsx @@ -276,7 +276,7 @@ const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => { { id: 'kandang', accessorKey: 'kandang', - header: 'Kandang', + header: 'Kandang Atribusi', cell: (props) => { const kandang = props.getValue() as Kandang; return kandang?.name || '-'; diff --git a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx index 23e3e8b0..e31c29a9 100644 --- a/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx +++ b/src/components/pages/closing/table/sapronak/OutgoingSapronaksTable.tsx @@ -127,11 +127,11 @@ const ClosingOutgoingSapronaksTable = ({ }, { accessorKey: 'source_warehouse', - header: 'Gudang Asal', + header: 'Gudang Asal (Fisik)', }, { accessorKey: 'destination_warehouse', - header: 'Gudang Tujuan', + header: 'Gudang Tujuan (Fisik)', }, { accessorKey: 'quantity', diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 540a3eca..911c9e9a 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -746,7 +746,7 @@ const MarketingTable = () => { } columns={[ { - header: 'Kandang', + header: 'Gudang Fisik', accessorFn(row) { return row.product_warehouse.warehouse.name; }, diff --git a/src/components/pages/marketing/SalesOrderFormModal.tsx b/src/components/pages/marketing/SalesOrderFormModal.tsx index d80b98c5..8fc4a031 100644 --- a/src/components/pages/marketing/SalesOrderFormModal.tsx +++ b/src/components/pages/marketing/SalesOrderFormModal.tsx @@ -207,7 +207,6 @@ const SalesOrderFormModal = ({ return { vehicle_number: product.vehicle_number as string, - kandang_id: product.kandang_id as number, product_warehouse_id: product.product_warehouse_id as number, unit_price: parseFloat(String(product.unit_price || 0)), total_weight: parseFloat(String(product.total_weight || 0)), diff --git a/src/components/pages/marketing/form/MarketingForm.schema.ts b/src/components/pages/marketing/form/MarketingForm.schema.ts index 17b6d78c..144ec6ff 100644 --- a/src/components/pages/marketing/form/MarketingForm.schema.ts +++ b/src/components/pages/marketing/form/MarketingForm.schema.ts @@ -13,6 +13,7 @@ import { Marketing, } from '@/types/api/marketing/marketing'; import { formatDate, formatTitleCase } from '@/lib/helper'; +import { getProductWarehouseOptionLabel } from '@/lib/product-warehouse'; type MarketingSchemaType = { customer_id: number | undefined; @@ -97,17 +98,21 @@ export type DeliveryOrderFormValues = Yup.InferType; export const SalesProductToFieldValues = ( product: BaseSalesOrder ): SalesOrderProductFormValues => { + const warehouseOption = { + value: product.product_warehouse.warehouse.id, + label: product.product_warehouse.warehouse.name, + }; + return { id: product.id, vehicle_number: product.vehicle_number, + warehouse_id: product.product_warehouse.warehouse.id, + warehouse: warehouseOption, kandang_id: product.product_warehouse.warehouse.id, - kandang: { - value: product.product_warehouse.warehouse.id, - label: product.product_warehouse.warehouse.name, - }, + kandang: warehouseOption, product_warehouse: { value: product.product_warehouse.id, - label: product.product_warehouse.product.name, + label: getProductWarehouseOptionLabel(product.product_warehouse), }, product_warehouse_data: product.product_warehouse, product_warehouse_id: product.product_warehouse.id, @@ -142,6 +147,10 @@ export const DeliveryProductToFieldValues = ( const soId = salesOrders.find( (so) => so.product_warehouse.id === item.product_warehouse.id )?.id; + const warehouseOption = { + value: item.product_warehouse.warehouse.id, + label: item.product_warehouse.warehouse.name, + }; return { id: soId, unit_price: item.unit_price, @@ -156,15 +165,15 @@ export const DeliveryProductToFieldValues = ( marketing_product: { id: soId, vehicle_number: item.vehicle_number, + warehouse_id: item.product_warehouse.warehouse.id, + warehouse: warehouseOption, kandang_id: item.product_warehouse.warehouse.id, - kandang: { - value: item.product_warehouse.warehouse.id, - label: item.product_warehouse.warehouse.name, - }, + kandang: warehouseOption, product_warehouse: { value: item.product_warehouse.id, - label: item.product_warehouse.product.name, + label: getProductWarehouseOptionLabel(item.product_warehouse), }, + product_warehouse_data: item.product_warehouse, product_warehouse_id: item.product_warehouse.id, unit_price: item.unit_price, total_weight: item.total_weight, diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx index c8bae43a..66b50600 100644 --- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx +++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx @@ -112,7 +112,7 @@ const DeliveryOrderProductForm = ({ if (!Boolean(item.qty)) { return { value: item.id, - label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`, + label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.warehouse?.label ?? item.marketing_product?.kandang?.label}`, } as OptionType; } else { return null; @@ -333,7 +333,7 @@ const DeliveryOrderProductForm = ({ if (initialValues?.marketing_product_id) { setSelectedProduct({ value: initialValues?.id, - label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.kandang?.label}`, + label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.warehouse?.label ?? initialValues?.marketing_product?.kandang?.label}`, } as OptionType); } } @@ -472,7 +472,11 @@ const DeliveryOrderProductForm = ({ text={ exisitingValues?.find( (item) => item.id === selectedProduct?.value - )?.marketing_product?.kandang?.label ?? '' + )?.marketing_product?.warehouse?.label ?? + exisitingValues?.find( + (item) => item.id === selectedProduct?.value + )?.marketing_product?.kandang?.label ?? + '' } color='success' className={{ diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts index 390756e7..fcf96941 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema.ts @@ -3,6 +3,11 @@ import * as Yup from 'yup'; type SalesOrderProductSchemaType = { id?: number | undefined; + warehouse_id?: number; + warehouse?: { + value: number; + label: string; + } | null; kandang_id?: number; kandang?: { value: number; @@ -44,15 +49,22 @@ export const SalesOrderProductSchema: Yup.ObjectSchema(WarehouseApi.basePath, 'id', 'name'); + options: warehouseOptions, + isLoadingOptions: isLoadingWarehouseOptions, + setInputValue: setWarehouseSearchValue, + loadMore: loadMoreWarehouses, + } = useSelect(WarehouseApi.basePath, 'id', 'name'); // Options Week dari minggu 1 - 22 // const optionsWeek = useMemo(() => { @@ -147,7 +152,6 @@ const SalesOrderProductForm = ({ // }, []); const { - options: warehouseSourceOptions, rawData: warehouseSourceRawData, isLoadingOptions: isLoadingWarehouseSourceOptions, setInputValue: setWarehouseInputValue, @@ -158,30 +162,65 @@ const SalesOrderProductForm = ({ 'product.name', '', { - warehouse_id: formik.values.kandang_id?.toString() ?? '', + warehouse_id: formik.values.warehouse_id?.toString() ?? '', type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '', } ); const productOptionsFiltered = useMemo(() => { - return warehouseSourceOptions.filter( - (product) => - !exisitingValues - ?.map((item) => item.product_warehouse_id) - .includes(product.value) + if (!isResponseSuccess(warehouseSourceRawData)) { + return initialValues?.product_warehouse + ? [initialValues.product_warehouse] + : []; + } + + const selectedProductIds = new Set( + exisitingValues + ?.filter((item) => item.id !== initialValues?.id) + .map((item) => Number(item.product_warehouse_id)) + .filter((item) => item > 0) ?? [] ); - }, [warehouseSourceOptions, exisitingValues]); + + const options = warehouseSourceRawData.data + .filter((item: ProductWarehouse) => !selectedProductIds.has(item.id)) + .map((item: ProductWarehouse) => ({ + value: item.id, + label: getProductWarehouseOptionLabel(item), + })); + + if ( + initialValues?.product_warehouse && + initialValues?.product_warehouse_id + ) { + const exists = options.find( + (option) => + Number(option.value) === Number(initialValues.product_warehouse_id) + ); + if (!exists) { + options.push(initialValues.product_warehouse); + } + } + + return options; + }, [warehouseSourceRawData, exisitingValues, initialValues]); // ===== Handler ===== - const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { - formik.setFieldValue('kandang', val as OptionType); - formik.setFieldValue('kandang_id', (val as OptionType)?.value); + const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { + const warehouse = (val as OptionType | null) ?? null; + + formik.setFieldValue('warehouse', warehouse); + formik.setFieldValue('warehouse_id', warehouse?.value); + formik.setFieldValue('kandang', warehouse); + formik.setFieldValue('kandang_id', warehouse?.value); formik.setFieldValue('product_warehouse_id', null); formik.setFieldValue('product_warehouse', null); + formik.setFieldValue('product_warehouse_data', null); formik.setFieldValue('qty', ''); }; - const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { + const productWarehouseChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { formik.setFieldValue('product_warehouse', val as OptionType); const newId = (val as OptionType)?.value; formik.setFieldValue('product_warehouse_id', newId); @@ -191,6 +230,7 @@ const SalesOrderProductForm = ({ (item: ProductWarehouse) => item.id === newId ); setSelectedProductWarehouse(productWarehouse || null); + formik.setFieldValue('product_warehouse_data', productWarehouse || null); formik.setFieldValue('qty', productWarehouse?.quantity); formik.setFieldValue('uom', productWarehouse?.product?.uom?.name || ''); if ( @@ -204,6 +244,8 @@ const SalesOrderProductForm = ({ } handleBlurField('qty'); } else { + setSelectedProductWarehouse(null); + formik.setFieldValue('product_warehouse_data', null); formik.setFieldValue('qty', ''); formik.setFieldValue('uom', ''); formik.setFieldValue('week', null); @@ -217,9 +259,12 @@ const SalesOrderProductForm = ({ formik.resetForm({ values: { vehicle_number: '', + warehouse_id: undefined, + warehouse: null, kandang_id: undefined, kandang: null, product_warehouse: null, + product_warehouse_data: null, product_warehouse_id: undefined, unit_price: '', total_weight: '', @@ -310,6 +355,10 @@ const SalesOrderProductForm = ({ handleBlurField('week'); }, [formik.values.week]); + useEffect(() => { + setSelectedProductWarehouse(initialValues?.product_warehouse_data || null); + }, [initialValues?.product_warehouse_data]); + return ( <>
- {/* Gudang */} + {/* Gudang Fisik */} {/* Kategori */} @@ -374,8 +423,9 @@ const SalesOrderProductForm = ({ value={formik.values.marketing_type} onChange={(val) => { formik.setFieldValue('marketing_type', val); - warehouseChangeHandler(null); + productWarehouseChangeHandler(null); formik.setFieldValue('product_warehouse', null); + formik.setFieldValue('product_warehouse_data', null); formik.setFieldValue('product_warehouse_id', null); formik.setFieldValue('convertion_unit', null); formik.setFieldValue('weight_per_convertion', null); @@ -392,18 +442,18 @@ const SalesOrderProductForm = ({ options={productOptionsFiltered} isLoading={isLoadingWarehouseSourceOptions} value={formik.values.product_warehouse} - onChange={warehouseChangeHandler} + onChange={productWarehouseChangeHandler} onInputChange={setWarehouseInputValue} onMenuScrollToBottom={loadMoreWarehouse} isClearable placeholder={ - formik.values.kandang_id + formik.values.warehouse_id ? productOptionsFiltered.length == 0 ? 'Tidak ada produk yang tersedia' : 'Pilih produk' - : 'Pilih Kandang Terlebih Dahulu' + : 'Pilih Gudang Fisik Terlebih Dahulu' } - isDisabled={!formik.values.kandang_id} + isDisabled={!formik.values.warehouse_id} isError={ formik.touched.product_warehouse_id && Boolean(formik.errors.product_warehouse_id) diff --git a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx index 71a6040c..5051d631 100644 --- a/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/DeliveryOrderProductTable.tsx @@ -104,9 +104,10 @@ const DeliveryOrderProductTable = ({ <> - Gudang + Gudang Fisik {doItem?.warehouse?.name || + item.marketing_product?.warehouse?.label || item.marketing_product?.product_warehouse_data?.warehouse?.name} @@ -235,7 +236,7 @@ const DeliveryOrderProductTable = ({ <> - Gudang + Gudang Fisik {item.warehouse?.name} diff --git a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx index 70282648..f40f9151 100644 --- a/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx +++ b/src/components/pages/marketing/form/table-view/SalesOrderProductTable.tsx @@ -73,8 +73,10 @@ const SalesOrderProductTable = ({ {item.vehicle_number} - Gudang - {item.kandang?.label} + Gudang Fisik + + {item.warehouse?.label ?? item.kandang?.label} + Kategori diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index b39f94ca..2a78ffe0 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -34,6 +34,7 @@ type RecordingGrowingFormSchemaType = { }[]; depletions: { product_warehouse_id?: number; + source_product_warehouse_id?: number; qty?: number | string; }[]; }; @@ -53,6 +54,7 @@ export type StockSchema = { export type DepletionSchema = { product_warehouse_id?: number; + source_product_warehouse_id?: number; qty?: number | string; }; @@ -77,6 +79,9 @@ const DepletionObjectSchema: Yup.ObjectSchema = Yup.object({ product_warehouse_id: Yup.number() .optional() .typeError('Depletions harus berupa angka!'), + source_product_warehouse_id: Yup.number() + .optional() + .typeError('Gudang sumber harus berupa angka!'), qty: Yup.number() .optional() .typeError('Jumlah depletions harus berupa angka!'), @@ -259,6 +264,7 @@ export const getRecordingGrowingFormInitialValues = ( depletion: NonNullable[0] ) => ({ product_warehouse_id: depletion.product_warehouse_id, + source_product_warehouse_id: depletion.source_product_warehouse_id, qty: depletion.qty, }) ) ?? [ diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 0d62fd0b..4595c05c 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -71,6 +71,10 @@ import { import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { formatDate, formatNumber, cn } from '@/lib/helper'; +import { + getProductWarehouseOptionLabel, + isProductWarehouseSelectableForKandang, +} from '@/lib/product-warehouse'; import toast from 'react-hot-toast'; import ApprovalSteps, { useApprovalSteps, @@ -202,15 +206,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { useState(''); const [stockProductsLocationId, setStockProductsLocationId] = useState(''); - const [stockProductsKandangId, setStockProductsKandangId] = - useState(''); const [depletionProductsLocationId, setDepletionProductsLocationId] = useState(''); - const [depletionProductsKandangId, setDepletionProductsKandangId] = - useState(''); const [eggProductsLocationId, setEggProductsLocationId] = useState(''); - const [eggProductsKandangId, setEggProductsKandangId] = useState(''); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); @@ -448,22 +447,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ? projectFlockKandangDetailData.data : undefined; - const selectedProjectFlockKandangId = useMemo(() => { - if (type === 'add') { - return projectFlockKandangLookup?.project_flock_kandang_id ?? null; + const selectedKandangId = useMemo(() => { + if (!selectedKandang?.value) { + return null; } - return ( - projectFlockKandangDetail?.id ?? - initialValues?.project_flock?.project_flock_kandang_id ?? - null - ); - }, [ - type, - projectFlockKandangLookup, - projectFlockKandangDetail, - initialValues, - ]); + return Number(selectedKandang.value); + }, [selectedKandang]); // ===== TRANSITION RESTRICTION LOGIC ===== const isTransitionPeriod = useMemo(() => { @@ -512,6 +502,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ?.filter((d) => d.product_warehouse_id && d.qty) .map((depletion) => ({ product_warehouse_id: depletion.product_warehouse_id!, + ...(depletion.source_product_warehouse_id && { + source_product_warehouse_id: + depletion.source_product_warehouse_id, + }), qty: Number(depletion.qty) || 0, })) : []; @@ -541,6 +535,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ?.filter((d) => d.product_warehouse_id && d.qty) .map((depletion) => ({ product_warehouse_id: depletion.product_warehouse_id!, + ...(depletion.source_product_warehouse_id && { + source_product_warehouse_id: depletion.source_product_warehouse_id, + }), qty: Number(depletion.qty) || 0, })); @@ -602,7 +599,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, []); const { - options: stockProductOptions, setInputValue: setStockProductInputValue, rawData: stockProducts, isLoadingOptions: isLoadingStockProducts, @@ -610,7 +606,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { flags: 'PAKAN,OVK', location_id: stockProductsLocationId, - kandang_id: stockProductsKandangId, }); const { @@ -619,7 +614,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { loadMore: loadMoreDepletionProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { location_id: depletionProductsLocationId, - kandang_id: depletionProductsKandangId, type: 'AYAM', }); @@ -686,7 +680,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { type: 'TELUR', location_id: eggProductsLocationId, - kandang_id: eggProductsKandangId, }); const approvedProjectFlockKandangsUrl = useMemo(() => { @@ -934,39 +927,46 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { projectFlockKandangDetail, ]); - const isProductWarehouseBelongsToSelectedProjectFlockKandang = useCallback( - (productWarehouse: ProductWarehouse) => { - if (!selectedProjectFlockKandangId) return false; + const appendProductWarehouseOption = useCallback( + (options: OptionType[], productWarehouse?: ProductWarehouse | null) => { + if (!productWarehouse) { + return; + } - return ( - productWarehouse.project_flock_kandang?.id === - selectedProjectFlockKandangId + const existingOption = options.find( + (opt) => Number(opt.value) === productWarehouse.id ); + + if (!existingOption) { + options.push({ + value: productWarehouse.id, + label: getProductWarehouseOptionLabel(productWarehouse), + }); + } }, - [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 buildProductWarehouseOptions = useCallback( + (productWarehouses: ProductWarehouse[]) => + productWarehouses + .filter((productWarehouse) => + isProductWarehouseSelectableForKandang( + productWarehouse, + selectedKandangId + ) + ) + .map((productWarehouse) => ({ + value: productWarehouse.id, + label: getProductWarehouseOptionLabel(productWarehouse), + })), + [selectedKandangId] + ); const unifiedStockProducts = useMemo(() => { - const options = selectedProjectFlockKandangId - ? stockProductOptions.filter((option) => - scopedStockProductIds.has(Number(option.value)) + const options = isResponseSuccess(stockProducts) + ? buildProductWarehouseOptions( + stockProducts.data as unknown as ProductWarehouse[] ) : []; @@ -977,113 +977,61 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { type !== 'add' ) { initialValues.stocks?.forEach((stock) => { - if (stock.product_warehouse && stock.product_warehouse.product) { - const existingOption = options.find( - (opt) => opt.value === stock.product_warehouse_id - ); - if (!existingOption) { - options.push({ - value: stock.product_warehouse_id, - label: stock.product_warehouse.product.name, - }); - } - } + appendProductWarehouseOption(options, stock.product_warehouse); }); } return options; }, [ - stockProductOptions, + stockProducts, + buildProductWarehouseOptions, initialValues, type, - selectedProjectFlockKandangId, - scopedStockProductIds, + appendProductWarehouseOption, ]); const depletionProducts = useMemo(() => { - const options: OptionType[] = []; - - if ( - isResponseSuccess(depletionProductsData) && - selectedProjectFlockKandangId - ) { - const data = depletionProductsData.data as unknown as ProductWarehouse[]; - data - .filter(isProductWarehouseBelongsToSelectedProjectFlockKandang) - .forEach((product) => { - options.push({ - value: product.id, - label: product.product.name, - }); - }); - } + const options = isResponseSuccess(depletionProductsData) + ? buildProductWarehouseOptions( + depletionProductsData.data as unknown as ProductWarehouse[] + ) + : []; if (initialValues && initialValues.depletions && type !== 'add') { initialValues.depletions.forEach((depletion) => { - if ( - depletion.product_warehouse && - depletion.product_warehouse.product - ) { - const existingOption = options.find( - (opt) => opt.value === depletion.product_warehouse_id - ); - if (!existingOption) { - options.push({ - value: depletion.product_warehouse_id, - label: depletion.product_warehouse.product.name, - }); - } - } + appendProductWarehouseOption(options, depletion.product_warehouse); }); } return options; }, [ depletionProductsData, + buildProductWarehouseOptions, initialValues, type, - selectedProjectFlockKandangId, - isProductWarehouseBelongsToSelectedProjectFlockKandang, + appendProductWarehouseOption, ]); const eggProducts = useMemo(() => { - const options: OptionType[] = []; - - if (isResponseSuccess(eggProductsData) && selectedProjectFlockKandangId) { - const data = eggProductsData.data as unknown as ProductWarehouse[]; - data - .filter(isProductWarehouseBelongsToSelectedProjectFlockKandang) - .forEach((product) => { - options.push({ - value: product.id, - label: product.product.name, - }); - }); - } + const options = isResponseSuccess(eggProductsData) + ? buildProductWarehouseOptions( + eggProductsData.data as unknown as ProductWarehouse[] + ) + : []; if (initialValues && initialValues.eggs && type !== 'add') { initialValues.eggs.forEach((egg) => { - if (egg.product_warehouse && egg.product_warehouse.product) { - const existingOption = options.find( - (opt) => opt.value === egg.product_warehouse_id - ); - if (!existingOption) { - options.push({ - value: egg.product_warehouse_id, - label: egg.product_warehouse.product.name, - }); - } - } + appendProductWarehouseOption(options, egg.product_warehouse); }); } return options; }, [ eggProductsData, + buildProductWarehouseOptions, initialValues, type, - selectedProjectFlockKandangId, - isProductWarehouseBelongsToSelectedProjectFlockKandang, + appendProductWarehouseOption, ]); // ===== FORMIK SETUP ===== @@ -1628,18 +1576,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } if (selectedLocation && kandang) { setStockProductsLocationId(selectedLocation.value.toString()); - setStockProductsKandangId(kandang.value.toString()); setDepletionProductsLocationId(selectedLocation.value.toString()); - setDepletionProductsKandangId(kandang.value.toString()); setEggProductsLocationId(selectedLocation.value.toString()); - setEggProductsKandangId(kandang.value.toString()); } else { setStockProductsLocationId(''); - setStockProductsKandangId(''); setDepletionProductsLocationId(''); - setDepletionProductsKandangId(''); setEggProductsLocationId(''); - setEggProductsKandangId(''); } formik.setFieldTouched('project_flock_kandang', true); formik.setFieldTouched('project_flock_kandang_id', true); @@ -1746,11 +1688,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedKandang(kandangOption); setStockProductsLocationId(location.id.toString()); - setStockProductsKandangId(kandang.id.toString()); setDepletionProductsLocationId(location.id.toString()); - setDepletionProductsKandangId(kandang.id.toString()); setEggProductsLocationId(location.id.toString()); - setEggProductsKandangId(kandang.id.toString()); if ( formik.values.project_flock_kandang_id !== diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx index c5e1a3a5..81c527f4 100644 --- a/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx +++ b/src/components/pages/report/marketing/export/DailyMarketingExportPDF.tsx @@ -81,7 +81,7 @@ const getTableColumns = ( }, { key: 'warehouse', - header: 'Gudang', + header: 'Gudang Fisik', flex: 1.2, align: 'left', cell: ({ row }) => row.warehouse?.name ?? '-', diff --git a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx index d43213f1..14c81bec 100644 --- a/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx +++ b/src/components/pages/report/marketing/export/DailyMarketingExportXLSX.tsx @@ -30,7 +30,7 @@ export const generateDailyMarketingExcel = async ( { header: 'Tanggal Jual', key: 'soDate', width: 15 }, { header: 'Tanggal Realisasi', key: 'realizationDate', width: 18 }, { header: 'Aging', key: 'aging', width: 10 }, - { header: 'Gudang', key: 'warehouse', width: 25 }, + { header: 'Gudang Fisik', key: 'warehouse', width: 25 }, { header: 'Pelanggan', key: 'customer', width: 25 }, { header: 'No. DO', key: 'doNumber', width: 15 }, { header: 'Sales/Marketing', key: 'sales', width: 20 }, @@ -97,7 +97,7 @@ export const generateDailyMarketingExcel = async ( }); } - worksheet.columns.forEach((column) => { + worksheet.columns.forEach((column: { width?: number }) => { if (column.width && column.width < 10) { column.width = 10; } diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 49bb798e..1d9dc750 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -508,7 +508,7 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { }, { id: 'warehouse', - header: 'Gudang', + header: 'Gudang Fisik', accessorKey: 'warehouse', cell: ({ row }) => row.original.warehouse.name, footer: () =>
-
, @@ -858,8 +858,8 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => { {/* Warehouse Filter */} { + if (!warehouse) { + return 'Gudang'; + } + + if (warehouse.type === 'KANDANG') { + return warehouse.kandang?.name + ? `Kandang ${warehouse.kandang.name}` + : 'Gudang Kandang'; + } + + if (warehouse.type === 'LOKASI') { + return 'Gudang Farm'; + } + + return 'Gudang Area'; +}; + +export const getProductWarehouseOptionLabel = ( + productWarehouse?: ProductWarehouse | null +): string => { + if (!productWarehouse) { + return ''; + } + + const productName = productWarehouse.product?.name || 'Produk'; + const warehouseName = productWarehouse.warehouse?.name || 'Gudang'; + const warehouseScope = getWarehouseScopeLabel(productWarehouse.warehouse); + + return `${productName} • ${warehouseName} (${warehouseScope})`; +}; + +export const isProductWarehouseSelectableForKandang = ( + productWarehouse: ProductWarehouse, + kandangId?: number | null +): boolean => { + const warehouse = productWarehouse.warehouse; + + if (!warehouse) { + return false; + } + + if (warehouse.type === 'LOKASI') { + return true; + } + + if (warehouse.type === 'KANDANG') { + return Boolean(kandangId) && warehouse.kandang?.id === kandangId; + } + + return false; +}; diff --git a/src/types/api/marketing/marketing.d.ts b/src/types/api/marketing/marketing.d.ts index a867d983..743493f7 100644 --- a/src/types/api/marketing/marketing.d.ts +++ b/src/types/api/marketing/marketing.d.ts @@ -5,7 +5,6 @@ import { CreatedUser, } from '@/types/api/api-general'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; -import { Kandang } from '@/types/api/master-data/kandang'; import { Warehouse } from '@/types/api/master-data/warehouse'; /** @@ -110,7 +109,8 @@ export type BaseCreateMarketingPayload = { export type BaseCreateMarketingProductPayload = { vehicle_number: string; - kandang_id: string | number | undefined; + warehouse_id?: string | number | undefined; + kandang_id?: string | number | undefined; product_warehouse_id: string | number | undefined; unit_price: string | number | undefined; total_weight: string | number | undefined; @@ -136,7 +136,8 @@ export type CreateSalesOrderPayload = BaseCreateMarketingPayload & { export type CreateSalesOrderProductPayload = BaseCreateMarketingProductPayload & { id?: number; - kandang?: Kandang | undefined; + warehouse?: Warehouse | undefined; + kandang?: Warehouse | undefined; product_warehouse?: ProductWarehouse | undefined; }; diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 8ce0ef15..04392ae4 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -55,6 +55,7 @@ export type BaseRecording = { export type RecordingDepletion = { product_warehouse_id: number; + source_product_warehouse_id?: number; qty: number; product_warehouse: ProductWarehouse; }; @@ -114,6 +115,7 @@ export type CreateGrowingRecordingPayload = { }[]; depletions?: { product_warehouse_id?: number; + source_product_warehouse_id?: number; qty?: number; }[]; }; From 8b1546a3054e79847d0a3a35c8341462c8070e4e Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Wed, 1 Apr 2026 11:03:56 +0700 Subject: [PATCH 02/21] codex/fix: purchase receivement error and recording doesn't show depletion/egg --- .../production/recording/form/RecordingForm.tsx | 12 ++++++++++++ src/lib/product-warehouse.ts | 6 +++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 4595c05c..3a54e77d 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -605,7 +605,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { loadMore: loadMoreStockProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { flags: 'PAKAN,OVK', + limit: '100', location_id: stockProductsLocationId, + ...(selectedKandangId + ? { kandang_id: selectedKandangId.toString() } + : {}), }); const { @@ -613,7 +617,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { isLoadingOptions: isLoadingDepletionProducts, loadMore: loadMoreDepletionProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { + limit: '100', location_id: depletionProductsLocationId, + ...(selectedKandangId + ? { kandang_id: selectedKandangId.toString() } + : {}), type: 'AYAM', }); @@ -678,8 +686,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { isLoadingOptions: isLoadingEggProducts, loadMore: loadMoreEggProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { + limit: '100', type: 'TELUR', location_id: eggProductsLocationId, + ...(selectedKandangId + ? { kandang_id: selectedKandangId.toString() } + : {}), }); const approvedProjectFlockKandangsUrl = useMemo(() => { diff --git a/src/lib/product-warehouse.ts b/src/lib/product-warehouse.ts index bd714223..8e3680da 100644 --- a/src/lib/product-warehouse.ts +++ b/src/lib/product-warehouse.ts @@ -50,7 +50,11 @@ export const isProductWarehouseSelectableForKandang = ( } if (warehouse.type === 'KANDANG') { - return Boolean(kandangId) && warehouse.kandang?.id === kandangId; + return ( + Boolean(kandangId) && + (warehouse.kandang?.id === kandangId || + productWarehouse.project_flock_kandang?.kandang_id === kandangId) + ); } return false; From 3d7a2073b0c32065ece8857767c7820ed2e54209 Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Wed, 1 Apr 2026 11:07:05 +0700 Subject: [PATCH 03/21] formatting --- .../production/recording/form/RecordingForm.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 3a54e77d..5190b610 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -607,9 +607,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { flags: 'PAKAN,OVK', limit: '100', location_id: stockProductsLocationId, - ...(selectedKandangId - ? { kandang_id: selectedKandangId.toString() } - : {}), + ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), }); const { @@ -619,9 +617,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { limit: '100', location_id: depletionProductsLocationId, - ...(selectedKandangId - ? { kandang_id: selectedKandangId.toString() } - : {}), + ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), type: 'AYAM', }); @@ -689,9 +685,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { limit: '100', type: 'TELUR', location_id: eggProductsLocationId, - ...(selectedKandangId - ? { kandang_id: selectedKandangId.toString() } - : {}), + ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), }); const approvedProjectFlockKandangsUrl = useMemo(() => { From f302bcdb4b8208f3bc7dee728e541893c539185a Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Wed, 1 Apr 2026 12:31:16 +0700 Subject: [PATCH 04/21] codex/fix: show farm stock usage on closing page --- .../recording/form/RecordingForm.tsx | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 5190b610..26a9aa1c 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1520,10 +1520,52 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { formik.setFieldValue('location_id', locationId); setSelectedLocation(location); + formik.setFieldTouched('project_flock', false, false); + formik.setFieldValue('project_flock', null); + formik.setFieldTouched('project_flock_id', false, false); + formik.setFieldValue('project_flock_id', 0); + formik.setFieldTouched('kandang', false, false); + formik.setFieldValue('kandang', null); + formik.setFieldTouched('kandang_id', false, false); + formik.setFieldValue('kandang_id', 0); + formik.setFieldTouched('project_flock_kandang', false, false); + formik.setFieldValue('project_flock_kandang', null); + formik.setFieldTouched('project_flock_kandang_id', false, false); + formik.setFieldValue('project_flock_kandang_id', 0); + formik.setFieldTouched('stocks', false, false); + formik.setFieldValue('stocks', [ + { + product_warehouse_id: 0, + qty: '', + }, + ]); + formik.setFieldTouched('depletions', false, false); + formik.setFieldValue('depletions', [ + { + product_warehouse_id: 0, + qty: '', + }, + ]); + if (isLayingCategory) { + formik.setFieldTouched('eggs', false, false); + formik.setFieldValue('eggs', [ + { + product_warehouse_id: 0, + qty: '', + weight: '', + }, + ]); + } + setSelectedStocks([]); + setSelectedDepletions([]); + setSelectedEggs([]); setSelectedProjectFlock(null); setSelectedKandang(null); setProductionStandards(null); setNextDayRecording(null); + setStockProductsLocationId(''); + setDepletionProductsLocationId(''); + setEggProductsLocationId(''); if (duplicateErrorShown) { toast.dismiss(); setDuplicateErrorShown(false); @@ -1546,10 +1588,48 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { formik.setFieldTouched('project_flock_id', true); formik.setFieldValue('project_flock_id', projectFlockId); + formik.setFieldTouched('kandang', false, false); + formik.setFieldValue('kandang', null); + formik.setFieldTouched('kandang_id', false, false); + formik.setFieldValue('kandang_id', 0); + formik.setFieldTouched('project_flock_kandang', false, false); + formik.setFieldValue('project_flock_kandang', null); + formik.setFieldTouched('project_flock_kandang_id', false, false); + formik.setFieldValue('project_flock_kandang_id', 0); + formik.setFieldTouched('stocks', false, false); + formik.setFieldValue('stocks', [ + { + product_warehouse_id: 0, + qty: '', + }, + ]); + formik.setFieldTouched('depletions', false, false); + formik.setFieldValue('depletions', [ + { + product_warehouse_id: 0, + qty: '', + }, + ]); + if (isLayingCategory) { + formik.setFieldTouched('eggs', false, false); + formik.setFieldValue('eggs', [ + { + product_warehouse_id: 0, + qty: '', + weight: '', + }, + ]); + } + setSelectedStocks([]); + setSelectedDepletions([]); + setSelectedEggs([]); setSelectedProjectFlock(projectFlock); setSelectedKandang(null); setProductionStandards(null); setNextDayRecording(null); + setStockProductsLocationId(''); + setDepletionProductsLocationId(''); + setEggProductsLocationId(''); if (duplicateErrorShown) { toast.dismiss(); setDuplicateErrorShown(false); @@ -1569,6 +1649,33 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { formik.setFieldTouched('kandang_id', true); formik.setFieldValue('kandang_id', kandangId); + formik.setFieldTouched('stocks', false, false); + formik.setFieldValue('stocks', [ + { + product_warehouse_id: 0, + qty: '', + }, + ]); + formik.setFieldTouched('depletions', false, false); + formik.setFieldValue('depletions', [ + { + product_warehouse_id: 0, + qty: '', + }, + ]); + if (isLayingCategory) { + formik.setFieldTouched('eggs', false, false); + formik.setFieldValue('eggs', [ + { + product_warehouse_id: 0, + qty: '', + weight: '', + }, + ]); + } + setSelectedStocks([]); + setSelectedDepletions([]); + setSelectedEggs([]); setSelectedKandang(kandang); setProductionStandards(null); setNextDayRecording(null); From 65f31f83400b14f3c6345393b7a3679a76aa81e2 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 1 Apr 2026 16:13:01 +0700 Subject: [PATCH 05/21] fix: parse to float numberFormatValues.value --- src/components/input/NumberInput.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx index e6e0e773..9eab9db6 100644 --- a/src/components/input/NumberInput.tsx +++ b/src/components/input/NumberInput.tsx @@ -35,7 +35,9 @@ const NumberInput = ({ | undefined; if (newChangeEvent) { - newChangeEvent.target.value = numberFormatValues.value; + newChangeEvent.target.value = parseFloat( + numberFormatValues.value + ) as unknown as string; onChange?.(newChangeEvent); } From edf21fbfc40411dc2bcdff8b740afe67bf423738 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Wed, 1 Apr 2026 16:13:13 +0700 Subject: [PATCH 06/21] fix: change from parseInt to parseFloat --- .../pages/master-data/product/form/ProductForm.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 01fa192c..72355c22 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -154,17 +154,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { sku: values.sku, uom_id: values.uom_id, product_category_id: values.product_category_id, - product_price: parseInt(values.product_price.toString()) || 0, + product_price: parseFloat(values.product_price.toString()) || 0, selling_price: values.selling_price - ? parseInt(values.selling_price.toString()) || 0 + ? parseFloat(values.selling_price.toString()) || 0 : undefined, - tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined, + tax: values.tax ? parseFloat(values.tax.toString()) || 0 : undefined, expiry_period: values.expiry_period - ? parseInt(values.expiry_period.toString()) || 0 + ? parseFloat(values.expiry_period.toString()) || 0 : undefined, suppliers: values.suppliers.map((s) => ({ supplier_id: s.supplier?.value as number, - price: parseInt(s.price.toString()) || 0, + price: parseFloat(s.price.toString()) || 0, })), flag: values.flag, sub_flags: values.sub_flags, From c040c0e9bb2f940d74e441d408ce395861392ebf Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 2 Apr 2026 09:51:22 +0700 Subject: [PATCH 07/21] feat: create PurchaseFilterModal component --- .../pages/purchase/PurchaseFilterModal.tsx | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/components/pages/purchase/PurchaseFilterModal.tsx diff --git a/src/components/pages/purchase/PurchaseFilterModal.tsx b/src/components/pages/purchase/PurchaseFilterModal.tsx new file mode 100644 index 00000000..a9cd00cd --- /dev/null +++ b/src/components/pages/purchase/PurchaseFilterModal.tsx @@ -0,0 +1,201 @@ +'use client'; + +import { RefObject, useState, useEffect } from 'react'; +import { useFormik } from 'formik'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Modal from '@/components/Modal'; +import Button from '@/components/Button'; +import DateInput from '@/components/input/DateInput'; +import SelectInputCheckbox from '@/components/input/SelectInputCheckbox'; + +import { OptionType, useSelect } from '@/components/input/SelectInput'; +import { PurchaseFilter } from '@/types/api/purchase/purchase'; +import { ProductCategory } from '@/types/api/master-data/product-category'; +import { ProductCategoryApi } from '@/services/api/master-data'; +import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line'; + +interface PurchaseFilterModalProps { + ref: RefObject; + onSubmit?: (values: PurchaseFilter) => void; + onReset?: () => void; +} + +const PurchaseFilterModal = ({ + ref, + onSubmit, + onReset, +}: PurchaseFilterModalProps) => { + const closeModalHandler = () => { + ref.current?.close(); + }; + + // ===== DATE ERROR STATE ===== + const [dateErrorShown, setDateErrorShown] = useState(false); + const [hasDateError, setHasDateError] = useState(false); + + // ===== CLEANUP TOAST ON UNMOUNT ===== + useEffect(() => { + return () => { + if (dateErrorShown) { + toast.dismiss(); + } + }; + }, [dateErrorShown]); + + // ===== CLEANUP TOAST WHEN MODAL CLOSES ===== + useEffect(() => { + const dialogElement = ref.current; + const handleModalClose = () => { + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }; + + dialogElement?.addEventListener('close', handleModalClose); + + return () => { + dialogElement?.removeEventListener('close', handleModalClose); + }; + }, [ref, dateErrorShown]); + + const { + setInputValue: setProductCategoryInputValue, + options: productCategoryOptions, + isLoadingOptions: isLoadingProductCategoryOptions, + loadMore: loadMoreProductCategory, + } = useSelect( + ProductCategoryApi.basePath, + 'id', + 'name', + 'search' + ); + + const formik = useFormik<{ + poDate: string; + category: { label: string; value: number }[]; + status: { label: string; value: string }[]; + }>({ + initialValues: { + poDate: '', + category: [], + status: [], + }, + onSubmit: async (values) => { + const formattedValues = { + ...values, + category: values.category.map((item) => String(item.value)), + status: values.status.map((item) => String(item.value)), + }; + + onSubmit?.(formattedValues); + closeModalHandler(); + }, + onReset: () => { + onReset?.(); + closeModalHandler(); + }, + }); + + const productCategoryChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + formik.setFieldValue('category', val); + }; + + const statusChangeHandler = (val: OptionType | OptionType[] | null) => { + formik.setFieldValue('status', val); + }; + + return ( + + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ + +
+ + {/* Modal Body */} +
+
+ + + + + ({ + label: item.step_name, + value: item.step_name, + }))} + /> +
+
+ + {/* Modal Footer */} +
+ + + +
+ +
+ ); +}; + +export default PurchaseFilterModal; From cae19d905b04cd3ccfcfc79e3ed8c68bad35ec1a Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 2 Apr 2026 09:57:40 +0700 Subject: [PATCH 08/21] feat: add filter modal --- .../pages/purchase/PurchaseTable.tsx | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 43ddab1d..d074a583 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -14,6 +14,7 @@ import useSWRInfinite from 'swr/infinite'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; +import Link from 'next/link'; import { Icon } from '@iconify/react'; import Table from '@/components/Table'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; @@ -25,18 +26,19 @@ import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton'; +import ButtonFilter from '@/components/helper/ButtonFilter'; +import PurchaseFilterModal from '@/components/pages/purchase/PurchaseFilterModal'; import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { BaseApiResponse } from '@/types/api/api-general'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { Purchase } from '@/types/api/purchase/purchase'; +import { Purchase, PurchaseFilter } from '@/types/api/purchase/purchase'; import { PurchaseApi } from '@/services/api/purchase'; import { ExpenseApi } from '@/services/api/expense'; import { Expense } from '@/types/api/expense'; import { Color } from '@/types/theme'; -import Link from 'next/link'; // ===== STATUS BADGE UTILITIES ===== const statusTextMap: Record = { @@ -165,14 +167,21 @@ const PurchaseTable = () => { } = useTableFilter({ initial: { search: '', + po_date: '', + approval_status: '', + product_category_id: '', }, paramMap: { page: 'page', pageSize: 'limit', + po_date: 'po_date', + approval_status: 'approval_status', + product_category_id: 'product_category_id', }, }); // ===== MODAL HOOKS ===== + const filterModal = useModal(); const deleteModal = useModal(); // ===== API DATA FETCHING ===== @@ -410,13 +419,17 @@ const PurchaseTable = () => { [updateFilter, setSearchValue] ); - // const pageSizeChangeHandler = useCallback( - // (val: OptionType | OptionType[] | null) => { - // const newVal = val as OptionType; - // setPageSize(newVal.value as number); - // }, - // [setPageSize] - // ); + const filterSubmitHandler = (values: PurchaseFilter) => { + updateFilter('po_date', values.poDate); + updateFilter('product_category_id', values.category.join(',')); + updateFilter('approval_status', values.status.join(',')); + }; + + const filterResetHandler = () => { + updateFilter('po_date', ''); + updateFilter('product_category_id', ''); + updateFilter('approval_status', ''); + }; return ( <> @@ -455,6 +468,20 @@ const PurchaseTable = () => { 'placeholder:font-semibold placeholder:text-base-content/50', }} /> + + @@ -513,6 +540,13 @@ const PurchaseTable = () => { {/* ===== MODAL COMPONENTS ===== */} + + + Date: Thu, 2 Apr 2026 09:57:48 +0700 Subject: [PATCH 09/21] feat: create PurchaseFilter type --- src/types/api/purchase/purchase.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index d39719a3..b0abe694 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -144,3 +144,9 @@ export type DeletePurchaseRequestItemPayload = { }; export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload; + +export type PurchaseFilter = { + poDate: string; + category: string[]; + status: string[]; +}; From e4b62387713ef44c8aa252f3df2b9965034d5483 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 2 Apr 2026 11:00:08 +0700 Subject: [PATCH 10/21] fix: use correct address --- src/components/pages/expense/pdf/ExpensePDF.tsx | 4 ++-- src/components/pages/marketing/pdf/DeliveryOrderExport.tsx | 4 ++-- src/components/pages/marketing/pdf/SalesOrderExport.tsx | 4 ++-- .../pages/report/expense/export/ReportExpenseExportPDF.tsx | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/pages/expense/pdf/ExpensePDF.tsx b/src/components/pages/expense/pdf/ExpensePDF.tsx index f76b6f11..d6e694a9 100644 --- a/src/components/pages/expense/pdf/ExpensePDF.tsx +++ b/src/components/pages/expense/pdf/ExpensePDF.tsx @@ -287,8 +287,8 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { PT LUMBUNG TELUR INDONESIA - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 + Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten + Bandung Barat, Jawa Barat 40514 diff --git a/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx index cdf18652..55420468 100644 --- a/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx +++ b/src/components/pages/marketing/pdf/DeliveryOrderExport.tsx @@ -101,8 +101,8 @@ const PDFDocument = ({ PT LUMBUNG TELUR INDONESIA - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 + Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten + Bandung Barat, Jawa Barat 40514 diff --git a/src/components/pages/marketing/pdf/SalesOrderExport.tsx b/src/components/pages/marketing/pdf/SalesOrderExport.tsx index 55eb3b5b..87021ba5 100644 --- a/src/components/pages/marketing/pdf/SalesOrderExport.tsx +++ b/src/components/pages/marketing/pdf/SalesOrderExport.tsx @@ -87,8 +87,8 @@ const PDFDocument = ({ data }: { data: Marketing }) => { PT LUMBUNG TELUR INDONESIA - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 + Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten + Bandung Barat, Jawa Barat 40514 diff --git a/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx index 352b0fa8..64d3178c 100644 --- a/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx +++ b/src/components/pages/report/expense/export/ReportExpenseExportPDF.tsx @@ -47,7 +47,7 @@ export const generateReportExpensePDF = async ( doc.setFontSize(7); doc.setTextColor(102, 102, 102); doc.text( - 'SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. Cipedes, Kec. Sukajadi, Kota Bandung 40162', + 'Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten Bandung Barat, Jawa Barat 40514', marginX, 25 ); From 10d1f05aa5cfb5d3854a2e6f3849a2813c5b837d Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Thu, 2 Apr 2026 11:00:17 +0700 Subject: [PATCH 11/21] fix: use correct address and logo --- .../pages/purchase/order/PurchaseOrderInvoice.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx index c87b65bc..a709530b 100644 --- a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx @@ -34,7 +34,7 @@ const pdfStyles = StyleSheet.create({ marginBottom: 20, }, logo: { - width: 120, + width: 30, height: 30, marginBottom: 8, }, @@ -265,7 +265,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { {/* eslint-disable-next-line jsx-a11y/alt-text */} @@ -273,8 +273,8 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { PT LUMBUNG TELUR INDONESIA - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 + Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten + Bandung Barat, Jawa Barat 40514 From c6d8533190fb463007f843d895f8b7deaa9ebeea Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Sat, 4 Apr 2026 09:53:10 +0700 Subject: [PATCH 12/21] codex/fix: inconsistent stock options and availability --- .../sales-order/SalesOrderProductForm.tsx | 4 +- .../recording/form/RecordingForm.tsx | 111 ++++++++++++++---- 2 files changed, 89 insertions(+), 26 deletions(-) diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index e3b96ac7..8bd48d7c 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -160,8 +160,10 @@ const SalesOrderProductForm = ({ ProductWarehouseApi.basePath, 'id', 'product.name', - '', + 'search', { + limit: '100', + available_only: 'true', warehouse_id: formik.values.warehouse_id?.toString() ?? '', type: formik.values.marketing_type?.value.toLocaleUpperCase() ?? '', } diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 26a9aa1c..379fe864 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -210,6 +210,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { useState(''); const [eggProductsLocationId, setEggProductsLocationId] = useState(''); + const [knownProductWarehouses, setKnownProductWarehouses] = useState< + Record + >({}); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); @@ -606,6 +609,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { flags: 'PAKAN,OVK', limit: '100', + available_only: 'true', location_id: stockProductsLocationId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), }); @@ -614,8 +618,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { rawData: depletionProductsData, isLoadingOptions: isLoadingDepletionProducts, loadMore: loadMoreDepletionProducts, - } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', '', { + } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { limit: '100', + available_only: 'true', location_id: depletionProductsLocationId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), type: 'AYAM', @@ -684,6 +689,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { limit: '100', type: 'TELUR', + available_only: 'true', location_id: eggProductsLocationId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), }); @@ -953,6 +959,77 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { [] ); + const mergeKnownProductWarehouses = useCallback( + (items: Array) => { + if (items.length === 0) { + return; + } + + setKnownProductWarehouses((prev) => { + const next = { ...prev }; + let changed = false; + + items.forEach((item) => { + if (!item?.id) { + return; + } + + if (next[item.id] !== item) { + next[item.id] = item; + changed = true; + } + }); + + return changed ? next : prev; + }); + }, + [] + ); + + useEffect(() => { + const items: Array = []; + + if (isResponseSuccess(stockProducts)) { + items.push(...((stockProducts.data as unknown as ProductWarehouse[]) ?? [])); + } + + if (isResponseSuccess(depletionProductsData)) { + items.push( + ...((depletionProductsData.data as unknown as ProductWarehouse[]) ?? []) + ); + } + + if (isResponseSuccess(eggProductsData)) { + items.push(...((eggProductsData.data as unknown as ProductWarehouse[]) ?? [])); + } + + initialValues?.stocks?.forEach((stock) => { + items.push((stock.product_warehouse as ProductWarehouse | undefined) ?? null); + }); + initialValues?.depletions?.forEach((depletion) => { + items.push( + (depletion.product_warehouse as ProductWarehouse | undefined) ?? null + ); + }); + initialValues?.eggs?.forEach((egg) => { + items.push((egg.product_warehouse as ProductWarehouse | undefined) ?? null); + }); + + mergeKnownProductWarehouses(items); + }, [ + stockProducts, + depletionProductsData, + eggProductsData, + initialValues, + mergeKnownProductWarehouses, + ]); + + const getKnownProductWarehouse = useCallback( + (productWarehouseId: number) => + knownProductWarehouses[productWarehouseId] ?? null, + [knownProductWarehouses] + ); + const buildProductWarehouseOptions = useCallback( (productWarehouses: ProductWarehouse[]) => productWarehouses @@ -965,7 +1042,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { .map((productWarehouse) => ({ value: productWarehouse.id, label: getProductWarehouseOptionLabel(productWarehouse), - })), + })) + .sort((a, b) => a.label.localeCompare(b.label)), [selectedKandangId] ); @@ -1245,12 +1323,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const getAvailableStock = useCallback( (productWarehouseId: number) => { if ((type as 'add' | 'edit' | 'detail') === 'detail') return 0; - if (!isResponseSuccess(stockProducts)) return 0; - const data = stockProducts.data as unknown as ProductWarehouse[]; - const productWarehouse = data.find((pw) => pw.id === productWarehouseId); + const productWarehouse = getKnownProductWarehouse(productWarehouseId); return productWarehouse?.quantity ?? 0; }, - [stockProducts, type] + [getKnownProductWarehouse, type] ); const getStockUsageError = useCallback( @@ -1344,10 +1420,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const getProductFlagBadgeAdornment = useCallback( (productWarehouseId: number) => { - if (!isResponseSuccess(stockProducts)) return null; - - const data = stockProducts.data as unknown as ProductWarehouse[]; - const productWarehouse = data.find((pw) => pw.id === productWarehouseId); + const productWarehouse = getKnownProductWarehouse(productWarehouseId); if (!productWarehouse) return null; const hasPakanFlag = productWarehouse.product.flags?.includes('PAKAN'); @@ -1363,7 +1436,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return null; }, - [stockProducts] + [getKnownProductWarehouse] ); const getProductUomSuffix = useCallback( @@ -1388,23 +1461,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } } - let rawData; - if (dataSource === 'stock') { - rawData = stockProducts; - } else if (dataSource === 'depletion') { - rawData = depletionProductsData; - } else if (dataSource === 'egg') { - rawData = eggProductsData; - } - - if (!isResponseSuccess(rawData)) return null; - - const data = rawData.data as unknown as ProductWarehouse[]; - const productWarehouse = data.find((pw) => pw.id === productWarehouseId); + const productWarehouse = getKnownProductWarehouse(productWarehouseId); return productWarehouse?.product.uom.name || null; }, - [stockProducts, depletionProductsData, eggProductsData, initialValues, type] + [getKnownProductWarehouse, initialValues, type] ); const getAvailableStockProductOptions = useCallback( From 107d412c102008cead0b445899faa718eee7ad70 Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Sat, 4 Apr 2026 09:57:01 +0700 Subject: [PATCH 13/21] formatting --- .../production/recording/form/RecordingForm.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 379fe864..9d0b44f0 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -990,7 +990,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const items: Array = []; if (isResponseSuccess(stockProducts)) { - items.push(...((stockProducts.data as unknown as ProductWarehouse[]) ?? [])); + items.push( + ...((stockProducts.data as unknown as ProductWarehouse[]) ?? []) + ); } if (isResponseSuccess(depletionProductsData)) { @@ -1000,11 +1002,15 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } if (isResponseSuccess(eggProductsData)) { - items.push(...((eggProductsData.data as unknown as ProductWarehouse[]) ?? [])); + items.push( + ...((eggProductsData.data as unknown as ProductWarehouse[]) ?? []) + ); } initialValues?.stocks?.forEach((stock) => { - items.push((stock.product_warehouse as ProductWarehouse | undefined) ?? null); + items.push( + (stock.product_warehouse as ProductWarehouse | undefined) ?? null + ); }); initialValues?.depletions?.forEach((depletion) => { items.push( @@ -1012,7 +1018,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); }); initialValues?.eggs?.forEach((egg) => { - items.push((egg.product_warehouse as ProductWarehouse | undefined) ?? null); + items.push( + (egg.product_warehouse as ProductWarehouse | undefined) ?? null + ); }); mergeKnownProductWarehouses(items); From 8872b283accb6e25facf3f989f44f255465dcf06 Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Mon, 6 Apr 2026 22:28:39 +0700 Subject: [PATCH 14/21] codex/fix: invisible depletion and egg <= 0 --- .../pages/production/recording/form/RecordingForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 9d0b44f0..846c6d7a 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -620,7 +620,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { loadMore: loadMoreDepletionProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { limit: '100', - available_only: 'true', + available_only: 'false', location_id: depletionProductsLocationId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), type: 'AYAM', @@ -688,8 +688,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { loadMore: loadMoreEggProducts, } = useSelect(ProductWarehouseApi.basePath, 'id', 'product.name', 'search', { limit: '100', + available_only: 'false', type: 'TELUR', - available_only: 'true', location_id: eggProductsLocationId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), }); From 981b48acc0dfcfaf40fe947fec408ceecc6c6817 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 7 Apr 2026 11:53:34 +0700 Subject: [PATCH 15/21] fix: check deep equality --- .../production/recording/form/RecordingForm.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 846c6d7a..680be036 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -974,9 +974,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return; } - if (next[item.id] !== item) { - next[item.id] = item; - changed = true; + const existing = next[item.id]; + if (existing !== item) { + // Check deep equality to avoid triggering state changes + // when identical data comes from different sources (e.g. initialValues vs SWR) + if ( + !existing || + JSON.stringify(existing) !== JSON.stringify(item) + ) { + next[item.id] = item; + changed = true; + } } }); From 129a3fda441d45e8dd5fb87d2c311a4da5bfd509 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 7 Apr 2026 11:54:00 +0700 Subject: [PATCH 16/21] fix: memoized formattedSuccessRawData and formattedErrorRawData --- src/components/input/SelectInput.tsx | 36 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/components/input/SelectInput.tsx b/src/components/input/SelectInput.tsx index 32f8dbcd..c1736fc5 100644 --- a/src/components/input/SelectInput.tsx +++ b/src/components/input/SelectInput.tsx @@ -566,23 +566,31 @@ const useSelect = ( setSize(size + 1); }; - let formattedSuccessRawData: SuccessApiResponse | undefined = undefined; - let formattedErrorRawData: ErrorApiResponse | undefined = undefined; - const latestPagesIndex = pages?.length ? pages.length - 1 : 0; - if (isResponseSuccess(pages?.[latestPagesIndex])) { - formattedSuccessRawData = { - ...pages?.[latestPagesIndex], - data: - pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ?? - [], - }; - } + const { formattedSuccessRawData, formattedErrorRawData } = useMemo(() => { + let successData: SuccessApiResponse | undefined = undefined; + let errorData: ErrorApiResponse | undefined = undefined; - if (isResponseError(pages?.[latestPagesIndex])) { - formattedErrorRawData = pages?.[latestPagesIndex]; - } + if (isResponseSuccess(pages?.[latestPagesIndex])) { + successData = { + ...pages![latestPagesIndex], + data: + pages?.flatMap((page) => + isResponseSuccess(page) ? page.data : [] + ) ?? [], + }; + } + + if (isResponseError(pages?.[latestPagesIndex])) { + errorData = pages![latestPagesIndex]; + } + + return { + formattedSuccessRawData: successData, + formattedErrorRawData: errorData, + }; + }, [pages, latestPagesIndex]); return { inputValue, From 922a93414f39c9c484ea9e69b1bc18c0af1bde00 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Tue, 7 Apr 2026 13:11:14 +0700 Subject: [PATCH 17/21] fix: adjust unit price placeholder --- .../form/repeater/sales-order/SalesOrderProductForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx index 8bd48d7c..5fd5c3cb 100644 --- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx +++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx @@ -758,7 +758,7 @@ const SalesOrderProductForm = ({ formik.touched.unit_price && Boolean(formik.errors.unit_price) } errorMessage={formik.errors.unit_price} - placeholder='Masukan Harga Satuan' + placeholder='Masukan Harga Satuan...' /> )} From ebf966228bc9ede5c71ef37dd9c094bc3395e5c9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 7 Apr 2026 15:23:31 +0700 Subject: [PATCH 18/21] refactor(FE-decimal-jumlah-pakai): Increase decimal scale for stock quantity input to 3 --- .../pages/production/recording/form/RecordingForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 680be036..51a704a4 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2853,7 +2853,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { value={stock.qty ?? ''} onChange={handleStockUsageQtyChangeWrapper(idx)} onBlur={formik.handleBlur} - decimalScale={0} + decimalScale={3} allowNegative={false} thousandSeparator=',' decimalSeparator='.' From 2f89c6f216cc912618561dd890e1957f28a5ada2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 7 Apr 2026 15:26:25 +0700 Subject: [PATCH 19/21] refactor(FE-unused-param): Comment out unused eggProductsLocationId state and references --- .../production/recording/form/RecordingForm.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 51a704a4..0ac34983 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -208,8 +208,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { useState(''); const [depletionProductsLocationId, setDepletionProductsLocationId] = useState(''); - const [eggProductsLocationId, setEggProductsLocationId] = - useState(''); + // const [eggProductsLocationId, setEggProductsLocationId] = + // useState(''); const [knownProductWarehouses, setKnownProductWarehouses] = useState< Record >({}); @@ -690,7 +690,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { limit: '100', available_only: 'false', type: 'TELUR', - location_id: eggProductsLocationId, + // location_id: eggProductsLocationId, ...(selectedKandangId ? { kandang_id: selectedKandangId.toString() } : {}), }); @@ -1642,7 +1642,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setNextDayRecording(null); setStockProductsLocationId(''); setDepletionProductsLocationId(''); - setEggProductsLocationId(''); + // setEggProductsLocationId(''); if (duplicateErrorShown) { toast.dismiss(); setDuplicateErrorShown(false); @@ -1706,7 +1706,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setNextDayRecording(null); setStockProductsLocationId(''); setDepletionProductsLocationId(''); - setEggProductsLocationId(''); + // setEggProductsLocationId(''); if (duplicateErrorShown) { toast.dismiss(); setDuplicateErrorShown(false); @@ -1767,11 +1767,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (selectedLocation && kandang) { setStockProductsLocationId(selectedLocation.value.toString()); setDepletionProductsLocationId(selectedLocation.value.toString()); - setEggProductsLocationId(selectedLocation.value.toString()); + // setEggProductsLocationId(selectedLocation.value.toString()); } else { setStockProductsLocationId(''); setDepletionProductsLocationId(''); - setEggProductsLocationId(''); + // setEggProductsLocationId(''); } formik.setFieldTouched('project_flock_kandang', true); formik.setFieldTouched('project_flock_kandang_id', true); @@ -1879,7 +1879,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setStockProductsLocationId(location.id.toString()); setDepletionProductsLocationId(location.id.toString()); - setEggProductsLocationId(location.id.toString()); + // setEggProductsLocationId(location.id.toString()); if ( formik.values.project_flock_kandang_id !== From 6e34eede4b81217ab7c759169fe550f6289a1a39 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 7 Apr 2026 16:01:13 +0700 Subject: [PATCH 20/21] refactor(FE-jumlah-pakai-zero-restriction): Update validation message for qty in StockObjectSchema --- .gitignore | 3 +++ .../pages/production/recording/form/RecordingForm.schema.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e47b8ec3..9e1fadc8 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ next-env.d.ts # claude .claude + +# rtk +rtk.exe diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 2a78ffe0..e4cf7a49 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -71,7 +71,7 @@ const StockObjectSchema: Yup.ObjectSchema = Yup.object({ .typeError('Produk harus berupa angka!'), qty: Yup.number() .required('Jumlah penggunaan wajib diisi!') - .min(1, 'Jumlah penggunaan tidak boleh 0!') + .moreThan(0, 'Jumlah penggunaan harus lebih dari 0!') .typeError('Jumlah penggunaan harus berupa angka!'), }); From b89730ab68a28c72c73475a3afd022b779326d8b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 7 Apr 2026 16:06:46 +0700 Subject: [PATCH 21/21] feat(FE-invalidate-mutation): Refactor SWR keys for recording detail pages and add cache invalidation --- src/app/production/recording/detail/edit/page.tsx | 7 +++++-- src/app/production/recording/detail/page.tsx | 7 +++++-- .../pages/production/recording/form/RecordingForm.tsx | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/app/production/recording/detail/edit/page.tsx b/src/app/production/recording/detail/edit/page.tsx index ad6c6a9a..aa5f1f46 100644 --- a/src/app/production/recording/detail/edit/page.tsx +++ b/src/app/production/recording/detail/edit/page.tsx @@ -11,10 +11,13 @@ const RecordingEdit = () => { const searchParams = useSearchParams(); const recordingId = searchParams.get('recordingId'); + const recordingDetailKey = recordingId + ? ['recording-detail', recordingId] + : null; const { data: recording, isLoading: isLoadingRecording } = useSWR( - recordingId, - (id: string) => RecordingApi.getSingle(parseInt(id)) + recordingDetailKey, + ([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id)) ); if (!recordingId) { diff --git a/src/app/production/recording/detail/page.tsx b/src/app/production/recording/detail/page.tsx index 194365a3..136c4283 100644 --- a/src/app/production/recording/detail/page.tsx +++ b/src/app/production/recording/detail/page.tsx @@ -11,10 +11,13 @@ const RecordingDetail = () => { const searchParams = useSearchParams(); const recordingId = searchParams.get('recordingId'); + const recordingDetailKey = recordingId + ? ['recording-detail', recordingId] + : null; const { data: recording, isLoading: isLoadingRecording } = useSWR( - recordingId, - (id: string) => RecordingApi.getSingle(parseInt(id)) + recordingDetailKey, + ([, id]: [string, string]) => RecordingApi.getSingle(parseInt(id)) ); if (!recordingId) { diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 0ac34983..2a044874 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -4,7 +4,7 @@ import { useMemo, useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; -import useSWR from 'swr'; +import useSWR, { useSWRConfig } from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -183,6 +183,7 @@ const productionStandardColumns: ColumnDef[] = [ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { // ===== HOOKS & ROUTER ===== const router = useRouter(); + const { mutate } = useSWRConfig(); // ===== STATE MANAGEMENT ===== const [selectedRecordDate, setSelectedRecordDate] = useState( @@ -319,11 +320,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setRecordingFormErrorMessage(res.message); return; } + await mutate(['recording-detail', recordingId.toString()]); toast.success(res?.message as string); router.refresh(); router.push('/production/recording'); }, - [router] + [mutate, router] ); const deleteRecordingClickHandler = useCallback(() => {