From ae0cca778e8e51511cb5dd341ad1f82966afe868 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 30 Oct 2025 18:10:04 +0700 Subject: [PATCH 001/101] chore(FE-Storyless): remove inputmask and its type definitions for cleanup --- package-lock.json | 27 +++++++++++---------------- package.json | 2 -- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56d046a8..e847e60b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", - "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -31,7 +30,6 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", - "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -1639,13 +1637,6 @@ "@types/react": "*" } }, - "node_modules/@types/inputmask": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz", - "integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1681,6 +1672,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1750,6 +1742,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2267,6 +2260,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2800,7 +2794,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.3.10", @@ -3228,6 +3223,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3401,6 +3397,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4203,12 +4200,6 @@ "node": ">=0.8.19" } }, - "node_modules/inputmask": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz", - "integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==", - "license": "MIT" - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5745,6 +5736,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5754,6 +5746,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6552,6 +6545,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6719,6 +6713,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 16d6c90c..17019b24 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", - "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -33,7 +32,6 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", - "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", From 4cb045de6c08fa25f03fa3b452c39321d35e906a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 30 Oct 2025 18:10:37 +0700 Subject: [PATCH 002/101] refactor(US-170,174): update recording types and validation schema for daily recording form --- .../recording/form/RecordingForm.schema.ts | 178 +++----- .../recording/form/RecordingForm.tsx | 386 +++++------------- src/types/api/production/recording.d.ts | 106 ++++- 3 files changed, 251 insertions(+), 419 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index ccb4ddd5..bd331793 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -1,16 +1,15 @@ import * as Yup from 'yup'; -import { RECORDING_FLAG_OPTIONS } from '@/config/constant'; import { Recording, - CreateRecordingPayload, + CreateGrowingRecordingPayload, } from '@/types/api/production/recording'; -export const RecordingFormSchema = Yup.object({ +export const RecordingGrowingFormSchema = Yup.object({ project_flock_kandang: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), }).nullable(), - project_flock_kandang_id: Yup.number() + project_flock_kandangs_id: Yup.number() .default(0) .typeError('Project Flock Kandang wajib diisi!') .test( @@ -22,9 +21,13 @@ export const RecordingFormSchema = Yup.object({ .test( 'not-already-recorded', 'Project Flock ini sudah direcord hari ini!', - function(value) { - const recordedProjectFlockIds = this.options.context?.recordedProjectFlockIds as Set; - const formType = this.options.context?.type as 'add' | 'edit' | 'detail'; + function (value) { + const recordedProjectFlockIds = this.options.context + ?.recordedProjectFlockIds as Set; + const formType = this.options.context?.type as + | 'add' + | 'edit' + | 'detail'; if (formType !== 'add') return true; if (value && recordedProjectFlockIds?.has(value)) { return false; @@ -35,20 +38,15 @@ export const RecordingFormSchema = Yup.object({ body_weights: Yup.array() .of( Yup.object({ - weight: Yup.number() - .required('Berat ayam wajib diisi!') - .min(1, 'Berat ayam minimal 1 gram!') - .typeError('Berat ayam harus berupa angka!'), + avg_weight: Yup.number() + .required('Berat ayam rata-rata wajib diisi!') + .min(1, 'Berat ayam rata-rata minimal 1 gram!') + .typeError('Berat ayam rata-rata harus berupa angka!'), qty: Yup.number() .required('Jumlah ayam wajib diisi!') .min(1, 'Jumlah ayam minimal 1 ekor!') .typeError('Jumlah ayam harus berupa angka!') .default(1), - average_weight: Yup.number() - .optional() - .min(0, 'Rata-rata berat tidak boleh negatif!') - .typeError('Rata-rata berat harus berupa angka!') - .default(0), }) ) .min(1, 'Minimal harus ada 1 data bobot badan!') @@ -60,163 +58,97 @@ export const RecordingFormSchema = Yup.object({ .required('Produk wajib diisi!') .min(1, 'Produk wajib diisi!') .typeError('Produk harus berupa angka!'), - usage_amount: Yup.number() + usage_qty: Yup.number() .required('Jumlah penggunaan wajib diisi!') .min(0, 'Jumlah penggunaan tidak boleh negatif!') .typeError('Jumlah penggunaan harus berupa angka!'), - notes: Yup.string().optional(), }) ) .min(1, 'Minimal harus ada 1 data stok!') .required('Data stok wajib diisi!'), depletions: Yup.array() - .of( - Yup.object({ - total: Yup.number() - .required('Jumlah depletions wajib diisi!') - .min(1, 'Jumlah depletions minimal 1!') - .typeError('Jumlah depletions harus berupa angka!'), - notes: Yup.string() - .required('Kondisi depletions wajib diisi!') - .oneOf( - RECORDING_FLAG_OPTIONS.map((option) => option.value), - 'Kondisi depletions tidak valid!' - ) - .typeError('Kondisi depletions harus berupa teks!') - .min(1, 'Kondisi depletions wajib diisi!'), - }) - ) - .min(1, 'Minimal harus ada 1 data depletions!') - .required('Data depletions wajib diisi!'), -}); - -export const UpdateRecordingFormSchema = Yup.object({ - project_flock_kandang: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - project_flock_kandang_id: Yup.number() - .default(0) - .typeError('Project Flock Kandang wajib diisi!') - .test( - 'is-valid-project-flock-kandang', - 'Project Flock Kandang wajib diisi!', - (value) => value !== undefined && value !== null && value > 0 - ) - .required('Project Flock Kandang wajib diisi!'), - body_weights: Yup.array() - .of( - Yup.object({ - weight: Yup.number() - .required('Berat ayam wajib diisi!') - .min(1, 'Berat ayam minimal 1 gram!') - .typeError('Berat ayam harus berupa angka!'), - qty: Yup.number() - .required('Jumlah ayam wajib diisi!') - .min(1, 'Jumlah ayam minimal 1 ekor!') - .typeError('Jumlah ayam harus berupa angka!') - .default(1), - average_weight: Yup.number() - .optional() - .min(0, 'Rata-rata berat tidak boleh negatif!') - .typeError('Rata-rata berat harus berupa angka!') - .default(0), - }) - ) - .min(1, 'Minimal harus ada 1 data bobot badan!') - .required('Data bobot badan wajib diisi!'), - stocks: Yup.array() .of( Yup.object({ product_warehouse_id: Yup.number() - .required('Produk wajib diisi!') - .min(1, 'Produk wajib diisi!') - .typeError('Produk harus berupa angka!'), - usage_amount: Yup.number() - .required('Jumlah penggunaan wajib diisi!') - .min(0, 'Jumlah penggunaan tidak boleh negatif!') - .typeError('Jumlah penggunaan harus berupa angka!'), - notes: Yup.string().optional(), - }) - ) - .min(1, 'Minimal harus ada 1 data stok!') - .required('Data stok wajib diisi!'), - depletions: Yup.array() - .of( - Yup.object({ - total: Yup.number() + .required('Produk depletions wajib diisi!') + .min(1, 'Produk depletions wajib diisi!') + .typeError('Produk depletions harus berupa angka!'), + qty: Yup.number() .required('Jumlah depletions wajib diisi!') .min(1, 'Jumlah depletions minimal 1!') .typeError('Jumlah depletions harus berupa angka!'), - notes: Yup.string() - .required('Kondisi depletions wajib diisi!') - .oneOf( - RECORDING_FLAG_OPTIONS.map((option) => option.value), - 'Kondisi depletions tidak valid!' - ) - .typeError('Kondisi depletions harus berupa teks!') - .min(1, 'Kondisi depletions wajib diisi!'), }) ) .min(1, 'Minimal harus ada 1 data depletions!') .required('Data depletions wajib diisi!'), }); -export type RecordingFormValues = Yup.InferType; +export const UpdateRecordingGrowingFormSchema = + RecordingGrowingFormSchema.shape({ + project_flock_kandangs_id: Yup.number() + .default(0) + .typeError('Project Flock Kandang wajib diisi!') + .test( + 'is-valid-project-flock-kandang', + 'Project Flock Kandang wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Project Flock Kandang wajib diisi!'), + }); + +export type RecordingGrowingFormValues = Yup.InferType< + typeof RecordingGrowingFormSchema +>; type RecordingFormData = Partial & { - body_weights?: CreateRecordingPayload['body_weights']; - stocks?: CreateRecordingPayload['stocks']; - depletions?: CreateRecordingPayload['depletions']; + body_weights?: CreateGrowingRecordingPayload['body_weights']; + stocks?: CreateGrowingRecordingPayload['stocks']; + depletions?: CreateGrowingRecordingPayload['depletions']; }; -export const getRecordingFormInitialValues = ( +export const getRecordingGrowingFormInitialValues = ( initialValues?: RecordingFormData -): RecordingFormValues => ({ - project_flock_kandang: initialValues?.project_flock_kandang_id +): RecordingGrowingFormValues => ({ + project_flock_kandang: initialValues?.project_flock_kandangs_id ? { - value: initialValues.project_flock_kandang_id, - label: `Project Flock #${initialValues.project_flock_kandang_id}`, + value: initialValues.project_flock_kandangs_id, + label: `Project Flock #${initialValues.project_flock_kandangs_id}`, } : null, - project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, + project_flock_kandangs_id: initialValues?.project_flock_kandangs_id ?? 0, body_weights: initialValues?.body_weights?.map( - (bw: NonNullable[0]) => ({ - weight: bw.weight, + (bw: NonNullable[0]) => ({ + avg_weight: bw.avg_weight, qty: bw.qty, - average_weight: bw.qty > 0 ? Math.round(bw.weight / bw.qty) : 0, }) ) ?? [ { - weight: 0, + avg_weight: 0, qty: 0, - average_weight: 0, }, ], stocks: initialValues?.stocks?.map( - (stock: NonNullable[0]) => ({ + (stock: NonNullable[0]) => ({ product_warehouse_id: stock.product_warehouse_id, - usage_amount: stock.usage_amount, - notes: stock.notes, + usage_qty: stock.usage_qty, }) ) ?? [ { product_warehouse_id: 0, - usage_amount: 0, - notes: '', + usage_qty: 0, }, ], depletions: initialValues?.depletions?.map( - (depletion: NonNullable[0]) => ({ + ( + depletion: NonNullable[0] + ) => ({ product_warehouse_id: depletion.product_warehouse_id, - total: depletion.total, - notes: depletion.notes, + qty: depletion.qty, }) ) ?? [ { product_warehouse_id: 0, - total: 0, - notes: '', + qty: 0, }, ], }); diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 7f3e6e85..161766f6 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useMemo, useState, useEffect, useCallback } from 'react'; +import { useMemo, useState, useCallback } from 'react'; import { useFormik } from 'formik'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; @@ -14,22 +14,21 @@ import { FormHeader } from '@/components/helper/form/FormHeader'; import { FormActions } from '@/components/helper/form/FormActions'; import { RecordingApi } from '@/services/api/production'; import { - CreateRecordingPayload, + CreateGrowingRecordingPayload, Recording, } from '@/types/api/production/recording'; import { type BaseApiResponse } from '@/types/api/api-general'; import { - RecordingFormSchema, - RecordingFormValues, - getRecordingFormInitialValues, - UpdateRecordingFormSchema, + RecordingGrowingFormSchema, + RecordingGrowingFormValues, + getRecordingGrowingFormInitialValues, + UpdateRecordingGrowingFormSchema, } from './RecordingForm.schema'; import { useRecordingFormHandlers } from './useRecordingFormHandlers'; import { ProjectFlockApi } from '@/services/api/production'; import { LocationApi } from '@/services/api/master-data'; import { ProductWarehouseApi } from '@/services/api/inventory'; import { isResponseSuccess } from '@/lib/api-helper'; -import { RECORDING_FLAG_OPTIONS } from '@/config/constant'; import { PeriodFlock } from '@/types/api/production/project-flock'; import { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; @@ -47,13 +46,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [selectedStocks, setSelectedStocks] = useState([]); const [selectedDepletions, setSelectedDepletions] = useState([]); - const [editingAverageIndex, setEditingAverageIndex] = useState( - null - ); - const [manuallyEditedRows, setManuallyEditedRows] = useState>( - new Set() - ); - const [locationSearchValue, setLocationSearchValue] = useState(''); const [selectedLocation, setSelectedLocation] = useState( null @@ -132,7 +124,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const recordedIds = new Set(); todayRecordings.forEach((recording) => { - const recordingDate = recording.record_date?.split('T')[0]; + const recordingDate = recording.record_datetime?.split('T')[0]; const isRecordedToday = recordingDate === today; @@ -143,7 +135,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { isResponseSuccess(projectFlocks) ) { const flockIndex = projectFlocks.data.findIndex( - (pf) => pf.id === recording.project_flock_kandang_id + (pf) => pf.id === recording.project_flock_kandangs_id ); if ( flockIndex !== undefined && @@ -165,7 +157,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } if (isRecordedToday && (isCorrectPeriod || !flockPeriodsData)) { - recordedIds.add(recording.project_flock_kandang_id); + recordedIds.add(recording.project_flock_kandangs_id); } }); @@ -283,45 +275,36 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { confirmationModalDeleteClickHandler, } = useRecordingFormHandlers(initialValues?.id); - const formikInitialValues = useMemo( - () => getRecordingFormInitialValues(initialValues), + const formikInitialValues = useMemo( + () => getRecordingGrowingFormInitialValues(initialValues), [initialValues] ); - const formik = useFormik({ + const formik = useFormik({ initialValues: formikInitialValues, validationSchema: - type === 'edit' ? UpdateRecordingFormSchema : RecordingFormSchema, + type === 'edit' + ? UpdateRecordingGrowingFormSchema + : RecordingGrowingFormSchema, validateOnChange: true, validateOnBlur: true, onSubmit: async (values) => { - const payload: CreateRecordingPayload = { - project_flock_kandang_id: values.project_flock_kandang_id, + const payload: CreateGrowingRecordingPayload = { + project_flock_kandangs_id: values.project_flock_kandangs_id, body_weights: (values.body_weights ?? []).map((bw) => ({ - weight: - typeof bw.weight === 'number' - ? bw.weight - : parseFloat(String(bw.weight)) || 0, - qty: - typeof bw.qty === 'number' - ? bw.qty - : parseFloat(String(bw.qty)) || 0, + avg_weight: + typeof bw.avg_weight === 'number' + ? bw.avg_weight + : parseFloat(String(bw.avg_weight)) || 0, + qty: bw.qty || 0, })), stocks: (values.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, - usage_amount: - typeof stock.usage_amount === 'number' - ? stock.usage_amount - : parseFloat(String(stock.usage_amount)) || 0, - notes: stock.notes || '', + usage_qty: stock.usage_qty || 0, })), depletions: (values.depletions ?? []).map((depletion) => ({ - product_warehouse_id: 1, - total: - typeof depletion.total === 'number' - ? depletion.total - : parseFloat(String(depletion.total)) || 0, - notes: depletion.notes, + product_warehouse_id: depletion.product_warehouse_id, + qty: depletion.qty || 0, })), }; @@ -375,7 +358,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const stock = formik.values.stocks?.[stockIdx]; if (!stock || !stock.product_warehouse_id) return null; const availableStock = getAvailableStock(stock.product_warehouse_id); - const requestedUsage = Number(stock.usage_amount) || 0; + const requestedUsage = Number(stock.usage_qty) || 0; if (requestedUsage > availableStock) { return `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`; } @@ -390,7 +373,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const stock = formik.values.stocks?.[stockIdx]; if (!stock || !stock.product_warehouse_id) return null; const availableStock = getAvailableStock(stock.product_warehouse_id); - const requestedUsage = Number(stock.usage_amount) || 0; + const requestedUsage = Number(stock.usage_qty) || 0; const remainingStock = availableStock - requestedUsage; if (requestedUsage > 0) { return ( @@ -432,7 +415,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color={color} size='sm' className={{ - badge: 'whitespace-nowrap font-semibold text-xs px-2 py-0.5', + badge: 'whitespace-nowrap font-semibold text-xs px-2', }} > Periode {projectFlock.period} @@ -461,7 +444,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='info' size='sm' className={{ - badge: 'whitespace-nowrap font-semibold text-xs px-2 py-0.5', + badge: 'whitespace-nowrap font-semibold text-xs px-2', }} > PAKAN @@ -476,7 +459,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { color='secondary' size='sm' className={{ - badge: 'whitespace-nowrap font-semibold text-xs px-2 py-0.5', + badge: 'whitespace-nowrap font-semibold text-xs px-2', }} > OVK @@ -503,11 +486,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { >( arrayName: T, column: T extends 'body_weights' - ? keyof RecordingFormValues['body_weights'][0] + ? keyof RecordingGrowingFormValues['body_weights'][0] : T extends 'stocks' - ? keyof RecordingFormValues['stocks'][0] + ? keyof RecordingGrowingFormValues['stocks'][0] : T extends 'depletions' - ? keyof RecordingFormValues['depletions'][0] + ? keyof RecordingGrowingFormValues['depletions'][0] : never, idx: number ) => { @@ -540,7 +523,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const locationChangeHandler = (val: OptionType | OptionType[] | null) => { setSelectedLocation(val as OptionType); formik.setFieldValue('project_flock_kandang', null); - formik.setFieldValue('project_flock_kandang_id', 0); + formik.setFieldValue('project_flock_kandangs_id', 0); }; const projectFlockKandangChangeHandler = ( @@ -557,9 +540,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { formik.setFieldTouched('project_flock_kandang', true); formik.setFieldValue('project_flock_kandang', val); - formik.setFieldTouched('project_flock_kandang_id', true); + formik.setFieldTouched('project_flock_kandangs_id', true); formik.setFieldValue( - 'project_flock_kandang_id', + 'project_flock_kandangs_id', (val as OptionType)?.value || 0 ); }; @@ -629,81 +612,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const newBodyWeights = [ ...(formik.values.body_weights || []), { - weight: 0, + avg_weight: 0, qty: 1, - average_weight: 0, }, ]; formik.setFieldValue('body_weights', newBodyWeights); }; - const handleWeightChange = (idx: number, value: number) => { - formik.setFieldValue(`body_weights.${idx}.weight`, value); - - setManuallyEditedRows((prev) => { - const newSet = new Set(prev); - newSet.delete(idx); - return newSet; - }); - - const currentWeight = formik.values.body_weights?.[idx]; - if (currentWeight) { - const qty = currentWeight.qty; - if (qty > 0 && value > 0) { - const averageWeight = parseFloat((value / qty).toFixed(2)); - formik.setFieldValue( - `body_weights.${idx}.average_weight`, - averageWeight - ); - } else { - formik.setFieldValue(`body_weights.${idx}.average_weight`, 0); - } - } + const handleAvgWeightChange = (idx: number, value: number) => { + formik.setFieldValue(`body_weights.${idx}.avg_weight`, value); }; const handleQtyChange = (idx: number, value: number) => { formik.setFieldValue(`body_weights.${idx}.qty`, value); - - setManuallyEditedRows((prev) => { - const newSet = new Set(prev); - newSet.delete(idx); - return newSet; - }); - - const currentWeight = formik.values.body_weights?.[idx]; - if (currentWeight) { - const weight = currentWeight.weight; - if (value > 0 && weight > 0) { - const averageWeight = parseFloat((weight / value).toFixed(2)); - formik.setFieldValue( - `body_weights.${idx}.average_weight`, - averageWeight - ); - } else { - formik.setFieldValue(`body_weights.${idx}.average_weight`, 0); - } - } }; - const handleAverageWeightChange = (idx: number, value: number) => { - formik.setFieldValue(`body_weights.${idx}.average_weight`, value); - - const currentWeight = formik.values.body_weights?.[idx]; - if (currentWeight) { - const qty = currentWeight.qty; - if (qty > 0 && value > 0) { - const totalWeight = value * qty; - formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight); - } else { - formik.setFieldValue(`body_weights.${idx}.weight`, 0); - } - } - }; - - const handleWeightChangeWrapper = + const handleAvgWeightChangeWrapper = (idx: number) => (e: React.ChangeEvent) => { const value = parseFloat(e.target.value) || 0; - handleWeightChange(idx, value); + handleAvgWeightChange(idx, value); }; const handleQtyChangeWrapper = @@ -712,19 +639,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { handleQtyChange(idx, value); }; - const handleAverageWeightChangeWrapper = - (idx: number) => (e: React.ChangeEvent) => { - setEditingAverageIndex(idx); - setManuallyEditedRows((prev) => new Set(prev).add(idx)); - - const value = parseFloat(e.target.value) || 0; - handleAverageWeightChange(idx, value); - }; - - const handleAverageWeightBlur = (idx: number) => { - setEditingAverageIndex(null); - }; - const removeBodyWeight = (idx: number) => { const updatedBodyWeights = formik.values.body_weights?.filter( (_, i) => i !== idx @@ -746,17 +660,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ...(formik.values.stocks || []), { product_warehouse_id: 0, - usage_amount: 0, - notes: '', + usage_qty: 0, }, ]; formik.setFieldValue('stocks', newStocks); }; - const handleStockUsageAmountChangeWrapper = useCallback( + const handleStockUsageQtyChangeWrapper = useCallback( (idx: number) => (e: React.ChangeEvent) => { const value = parseFloat(e.target.value) || 0; - formik.setFieldValue(`stocks.${idx}.usage_amount`, value); + formik.setFieldValue(`stocks.${idx}.usage_qty`, value); }, [formik] ); @@ -779,17 +692,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const newDepletions = [ ...(formik.values.depletions || []), { - total: 0, - notes: '', + product_warehouse_id: 0, + qty: 0, }, ]; formik.setFieldValue('depletions', newDepletions); }; - const handleDepletionTotalChangeWrapper = useCallback( + const handleDepletionQtyChangeWrapper = useCallback( (idx: number) => (e: React.ChangeEvent) => { const value = parseFloat(e.target.value) || 0; - formik.setFieldValue(`depletions.${idx}.total`, value); + formik.setFieldValue(`depletions.${idx}.qty`, value); }, [formik] ); @@ -809,41 +722,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedDepletions([]); }; - // ===== EFFECTS ===== - useEffect(() => { - if (formik.values.body_weights && editingAverageIndex === null) { - const updatedBodyWeights = formik.values.body_weights.map( - (weight, idx) => { - if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) { - return weight; - } - return { - ...weight, - average_weight: - weight.qty > 0 && weight.weight > 0 - ? parseFloat((weight.weight / weight.qty).toFixed(2)) - : 0, - }; - } - ); - const hasChanges = updatedBodyWeights.some( - (updated, idx) => - idx !== editingAverageIndex && - !manuallyEditedRows.has(idx) && - updated.average_weight !== - (formik.values.body_weights[idx]?.average_weight || 0) - ); - if (hasChanges) { - formik.setFieldValue('body_weights', updatedBodyWeights, false); - } - } - }, [ - formik.values.body_weights?.map((w) => w.weight), - formik.values.body_weights?.map((w) => w.qty), - editingAverageIndex, - manuallyEditedRows, - ]); - return ( <>
@@ -889,6 +767,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{ onInputChange={setProjectFlockSearchValue} isLoading={isLoadingProjectFlocks} isError={ - formik.touched.project_flock_kandang_id && - Boolean(formik.errors.project_flock_kandang_id) + formik.touched.project_flock_kandangs_id && + Boolean(formik.errors.project_flock_kandangs_id) } errorMessage={ - formik.errors.project_flock_kandang_id as string + formik.errors.project_flock_kandangs_id as string } isDisabled={type === 'detail' || !selectedLocation} placeholder={ @@ -911,9 +790,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { isClearable isSearchable startAdornment={ - formik.values.project_flock_kandang_id + formik.values.project_flock_kandangs_id ? getProjectFlockBadgeAdornment( - formik.values.project_flock_kandang_id + formik.values.project_flock_kandangs_id ) : undefined } @@ -964,7 +843,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { )} - Berat Ayam (gram) + Rata-rata Berat Ayam (gram) { * - - Rata-rata Berat Ayam (gram) - - - - {type !== 'detail' && Action} @@ -1029,9 +895,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { { decimalSeparator='.' inputSuffix='gram' isError={ - isRepeaterInputError('body_weights', 'weight', idx) - .isError + isRepeaterInputError( + 'body_weights', + 'avg_weight', + idx + ).isError } errorMessage={ - isRepeaterInputError('body_weights', 'weight', idx) - .errorMessage + isRepeaterInputError( + 'body_weights', + 'avg_weight', + idx + ).errorMessage } readOnly={type === 'detail'} className={{ @@ -1077,43 +949,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }} /> - - { - handleAverageWeightBlur(idx); - formik.handleBlur(e); - }} - decimalScale={2} - allowNegative={false} - thousandSeparator=',' - decimalSeparator='.' - inputSuffix='gram' - isError={ - isRepeaterInputError( - 'body_weights', - 'average_weight', - idx - ).isError - } - errorMessage={ - isRepeaterInputError( - 'body_weights', - 'average_weight', - idx - ).errorMessage - } - readOnly={type === 'detail'} - className={{ - wrapper: 'w-full min-w-32', - }} - /> - {type !== 'detail' && ( -
+
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
{selectedBodyWeights.length > 0 && (
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
{selectedStocks.length > 0 && (
- {type !== 'detail' && ( + {(type as 'add' | 'edit' | 'detail') !== 'detail' && (
{selectedDepletions.length > 0 && ( + )} + +
+ )} + + )} + {/* Action buttons */} type={type} @@ -1750,7 +2117,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { /> {/* Approve Confirmation Modal */} - {type === 'detail' && ( + {(type as 'add' | 'edit' | 'detail') === 'detail' && ( { )} {/* Reject Confirmation Modal */} - {type === 'detail' && ( + {(type as 'add' | 'edit' | 'detail') === 'detail' && ( Date: Fri, 31 Oct 2025 13:08:55 +0700 Subject: [PATCH 009/101] refactor(FE-170,174): simplify daily recording form by removing unused flock period logic --- .../recording/form/RecordingForm.tsx | 76 ++----------------- 1 file changed, 6 insertions(+), 70 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 5722e726..530b1d43 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -37,7 +37,6 @@ import { LocationApi } from '@/services/api/master-data'; import { ProductWarehouseApi } from '@/services/api/inventory'; import { isResponseSuccess } from '@/lib/api-helper'; import { - PeriodFlock, ProjectFlockKandangLookup, } from '@/types/api/production/project-flock'; import { useModal } from '@/components/Modal'; @@ -153,72 +152,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { RecordingApi.getAllFetcher ); - const flockPeriodsUrls = useMemo(() => { - if (!isResponseSuccess(projectFlocks)) return []; - return ( - projectFlocks?.data.map( - (pf) => `${ProjectFlockApi.basePath}/flocks/${pf.flock.id}/periods` - ) || [] - ); - }, [projectFlocks]); - - const { data: flockPeriodsData } = useSWR[]>( - flockPeriodsUrls.length > 0 ? flockPeriodsUrls : null, - (urls: string[]) => - Promise.all(urls.map((url) => fetch(url).then((res) => res.json()))), - { - revalidateOnFocus: false, - dedupingInterval: 60000, - } - ); - - const recordedProjectFlockIds = useMemo(() => { - if (!isResponseSuccess(existingRecordings)) return new Set(); - - const todayRecordings = existingRecordings?.data || []; - const recordedIds = new Set(); - - todayRecordings.forEach((recording) => { - const recordingDate = recording.record_datetime?.split('T')[0]; - - const isRecordedToday = recordingDate === today; - - let isCorrectPeriod = false; - if ( - isRecordedToday && - flockPeriodsData && - isResponseSuccess(projectFlocks) - ) { - const flockIndex = projectFlocks.data.findIndex( - (pf) => pf.id === recording.project_flock_kandangs_id - ); - if ( - flockIndex !== undefined && - flockIndex >= 0 && - flockPeriodsData[flockIndex] - ) { - const flockPeriod = flockPeriodsData[flockIndex]; - const currentProjectFlock = projectFlocks.data[flockIndex]; - - if ( - currentProjectFlock && - isResponseSuccess(flockPeriod) && - flockPeriod.data?.next_period - ) { - const expectedDay = flockPeriod.data.next_period - 1; - isCorrectPeriod = recording.day === expectedDay; - } - } - } - - if (isRecordedToday && (isCorrectPeriod || !flockPeriodsData)) { - recordedIds.add(recording.project_flock_kandangs_id); - } - }); - - return recordedIds; - }, [existingRecordings, today, flockPeriodsData, projectFlocks]); - + const { data: stockProducts, isLoading: isLoadingStockProducts } = useSWR( stockProductsUrl, ProductWarehouseApi.getAllFetcher @@ -592,11 +526,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); const getProjectFlockBadgeAdornment = useCallback(() => { - if (!isResponseSuccess(projectFlocks) || !projectFlockKandangLookup) + if (!projectFlockKandangLookup) return null; const isAlreadyRecorded = recordedProjectFlockKandangIds.has( - projectFlockKandangLookup.id + projectFlockKandangLookup.project_flock_kandang_id ); let color: 'neutral' | 'success' | 'warning' | 'error' = 'neutral'; @@ -619,7 +553,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); }, [ - projectFlocks, recordedProjectFlockKandangIds, projectFlockKandangLookup, ]); @@ -1124,6 +1057,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {(type as 'add' | 'edit' | 'detail') === 'detail' ? null : ( <> { /> { /> Date: Fri, 31 Oct 2025 13:52:36 +0700 Subject: [PATCH 010/101] feat(FE-174): add FormStepStatus type to enhance daily recording form state management --- src/types/api/api-general.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/types/api/api-general.d.ts b/src/types/api/api-general.d.ts index a42eaa3f..bdc2cffc 100644 --- a/src/types/api/api-general.d.ts +++ b/src/types/api/api-general.d.ts @@ -115,3 +115,9 @@ export type BaseApproval = { }; export type ApproveAction = 'APPROVED' | 'REJECTED'; + +export type FormStepStatus = { + name: string; + isCompleted: boolean; + isCurrent: boolean; +}; From c486d6cf817addf32a2a117d57857df6eb016550 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 31 Oct 2025 13:52:54 +0700 Subject: [PATCH 011/101] feat(FE-170): implement form steps and navigation for daily recording form --- .../recording/form/RecordingForm.tsx | 278 +++++++++++++++--- 1 file changed, 234 insertions(+), 44 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 530b1d43..fe1183c7 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1,6 +1,7 @@ 'use client'; import { useMemo, useState, useEffect, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; import { useFormik } from 'formik'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; @@ -11,7 +12,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { FormHeader } from '@/components/helper/form/FormHeader'; -import { FormActions } from '@/components/helper/form/FormActions'; import { RecordingApi } from '@/services/api/production'; import { CreateGrowingRecordingPayload, @@ -20,7 +20,7 @@ import { UpdateLayingRecordingPayload, Recording, } from '@/types/api/production/recording'; -import { type BaseApiResponse } from '@/types/api/api-general'; +import { type BaseApiResponse, FormStepStatus } from '@/types/api/api-general'; import { RecordingGrowingFormSchema, RecordingLayingFormSchema, @@ -36,14 +36,15 @@ import { ProjectFlockApi } from '@/services/api/production'; import { LocationApi } from '@/services/api/master-data'; import { ProductWarehouseApi } from '@/services/api/inventory'; import { isResponseSuccess } from '@/lib/api-helper'; -import { - ProjectFlockKandangLookup, -} from '@/types/api/production/project-flock'; +import { cn } from '@/lib/helper'; +import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; import { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; +import Steps from '@/components/steps/Steps'; +import StepItem from '@/components/steps/StepItem'; import { Kandang } from '@/types/api/master-data/kandang'; interface RecordingFormProps { @@ -52,6 +53,7 @@ interface RecordingFormProps { } const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { + const router = useRouter(); const [selectedBodyWeights, setSelectedBodyWeights] = useState([]); const [selectedStocks, setSelectedStocks] = useState([]); const [selectedDepletions, setSelectedDepletions] = useState([]); @@ -75,6 +77,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); + const [formSteps, setFormSteps] = useState(null); const approveModal = useModal(); const rejectModal = useModal(); @@ -152,7 +155,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { RecordingApi.getAllFetcher ); - const { data: stockProducts, isLoading: isLoadingStockProducts } = useSWR( stockProductsUrl, ProductWarehouseApi.getAllFetcher @@ -526,8 +528,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); const getProjectFlockBadgeAdornment = useCallback(() => { - if (!projectFlockKandangLookup) - return null; + if (!projectFlockKandangLookup) return null; const isAlreadyRecorded = recordedProjectFlockKandangIds.has( projectFlockKandangLookup.project_flock_kandang_id @@ -552,10 +553,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Periode {projectFlockKandangLookup.project_flock?.period} ); - }, [ - recordedProjectFlockKandangIds, - projectFlockKandangLookup, - ]); + }, [recordedProjectFlockKandangIds, projectFlockKandangLookup]); const getProductFlagBadgeAdornment = useCallback( (productWarehouseId: number) => { @@ -713,9 +711,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (isResponseSuccess(approveResponse)) { toast.success('Recording berhasil disetujui!'); approveModal.closeModal(); - if (typeof window !== 'undefined') { - window.location.href = '/production/recording'; - } + router.push('/production/recording'); } else { toast.error( (approveResponse?.message as string) || 'Gagal menyetujui recording' @@ -743,9 +739,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (isResponseSuccess(rejectResponse)) { toast.success('Recording berhasil ditolak!'); rejectModal.closeModal(); - if (typeof window !== 'undefined') { - window.location.href = '/production/recording'; - } + router.push('/production/recording'); } else { toast.error( (rejectResponse?.message as string) || 'Gagal menolak recording' @@ -978,6 +972,26 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { } }, [isLayingCategory, type, formik]); + useEffect(() => { + if (isLayingCategory) { + const steps: FormStepStatus[] = [ + { + name: 'Recording', + isCompleted: type === 'detail', + isCurrent: type !== 'detail', + }, + { + name: 'Grading', + isCompleted: false, + isCurrent: type === 'detail', + }, + ]; + setFormSteps(steps); + } else { + setFormSteps(null); + } + }, [isLayingCategory, type]); + useEffect(() => { if (formik.values.body_weights && editingAverageIndex === null) { const updatedBodyWeights = formik.values.body_weights.map( @@ -1023,16 +1037,79 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Project Flock Info Card */} {projectFlockKandangLookup && ( - - {projectFlockKandangLookup.project_flock.category} - +
+ + {projectFlockKandangLookup.project_flock.category} + + + {/* Form Steps for LAYING Category */} + {formSteps && ( +
+
+
    + {formSteps.map((step, idx) => ( + + ) : ( + idx + 1 + ) + } + > + {step.name} + + ))} +
+
+ + {/* Action buttons for multi-form navigation */} + {type === 'detail' && ( +
+ + +
+ )} +
+ )} +
)} { )} {/* Action buttons */} - - type={type} - formik={formik} - editUrl={ - initialValues - ? `/production/recording/detail/edit/?recordingId=${initialValues.id}` - : undefined - } - onDelete={deleteRecordingClickHandler} - onApprove={() => approveModal.openModal()} - onReject={() => rejectModal.openModal()} - isApproveLoading={isApproveLoading} - isRejectLoading={isRejectLoading} - showApproveReject={type === 'detail'} - disableSubmit={hasExceededStock} - /> +
+ {type !== 'add' && ( +
+ {deleteRecordingClickHandler && ( + + )} + {type !== 'edit' && initialValues && ( + + )} + {type === 'detail' && ( + <> + + + + )} +
+ )} + {type !== 'detail' && ( +
+ + + {isLayingCategory && ( + + )} +
+ )} +
{recordingFormErrorMessage && (
Date: Fri, 31 Oct 2025 14:01:51 +0700 Subject: [PATCH 012/101] feat(FE-174): add grading functionality to daily recording form with validation --- .../recording/form/RecordingForm.schema.ts | 49 +++++++++++++++++++ src/types/api/production/recording.d.ts | 6 +-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 7bb1cd26..fb58b048 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -4,6 +4,7 @@ import { CreateGrowingRecordingPayload, CreateLayingRecordingPayload, CreateEggPayload, + CreateGradingPayload, } from '@/types/api/production/recording'; export const RecordingGrowingFormSchema = Yup.object({ @@ -132,6 +133,35 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({ .required('Project Flock Kandang wajib diisi!'), }); +export const RecordingGradingFormSchema = Yup.object({ + recording_egg_id: Yup.number() + .required('Recording Egg ID wajib diisi!') + .min(1, 'Recording Egg ID minimal 1!') + .typeError('Recording Egg ID harus berupa angka!'), + eggs_grading: Yup.array() + .of( + Yup.object({ + grade: Yup.string() + .required('Grade telur wajib diisi!') + .typeError('Grade telur harus berupa string!'), + qty: Yup.number() + .required('Jumlah telur wajib diisi!') + .min(1, 'Jumlah telur minimal 1!') + .typeError('Jumlah telur harus berupa angka!'), + }) + ) + .min(1, 'Minimal harus ada 1 data grading telur!') + .required('Data grading telur wajib diisi!'), +}); + +export const UpdateRecordingGradingFormSchema = + RecordingGradingFormSchema.shape({ + recording_egg_id: Yup.number() + .required('Recording Egg ID wajib diisi!') + .min(1, 'Recording Egg ID minimal 1!') + .typeError('Recording Egg ID harus berupa angka!'), + }); + export type RecordingGrowingFormValues = Yup.InferType< typeof RecordingGrowingFormSchema >; @@ -140,6 +170,10 @@ export type RecordingLayingFormValues = Yup.InferType< typeof RecordingLayingFormSchema >; +export type RecordingGradingFormValues = Yup.InferType< + typeof RecordingGradingFormSchema +>; + type RecordingFormData = Partial & { body_weights?: CreateGrowingRecordingPayload['body_weights']; stocks?: CreateGrowingRecordingPayload['stocks']; @@ -211,3 +245,18 @@ export const getRecordingLayingFormInitialValues = ( }, ], }); + +export const getRecordingGradingFormInitialValues = ( + initialValues?: Partial +): RecordingGradingFormValues => ({ + recording_egg_id: initialValues?.recording_egg_id ?? 0, + eggs_grading: initialValues?.eggs_grading?.map((grading) => ({ + grade: grading.grade, + qty: grading.qty, + })) ?? [ + { + grade: '', + qty: 0, + }, + ], +}); diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 42a02c09..55272533 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -86,9 +86,8 @@ export type CreateGrowingRecordingPayload = { }; export type CreateGradingPayload = { - recording_id: number; - grading: { - product_warehouse_id: number; + recording_egg_id: number; + eggs_grading: { grade: string; qty: number; }[]; @@ -110,5 +109,6 @@ export type CreateRecordingPayload = export type UpdateGrowingRecordingPayload = CreateGrowingRecordingPayload; export type UpdateLayingRecordingPayload = CreateLayingRecordingPayload; +export type UpdateGradingRecordingPayload = CreateGradingRecordingPayload; export type UpdateRecordingPayload = CreateRecordingPayload; From 4a1f775c8544ca5a42d4d56f69631f9484bc7097 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 31 Oct 2025 15:15:32 +0700 Subject: [PATCH 013/101] feat(FE-170): update daily recording form to redirect to grading form after successful submission --- .../pages/production/recording/form/RecordingForm.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index fe1183c7..f024c3bf 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1089,7 +1089,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { type='button' color='primary' onClick={() => { - toast.success('Akan dialihkan ke form Grading'); + router.push(`/production/recording/grading/add?recording_id=${initialValues?.id}`); }} > Lanjut ke Grading @@ -2192,7 +2192,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { hasExceededStock || !formik.isValid || formik.isSubmitting } onClick={async () => { - await formik.submitForm(); + const result = await formik.submitForm(); if ( formik.isValid && !formik.isSubmitting && @@ -2201,8 +2201,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { toast.success( 'Recording berhasil disimpan! Mengalihkan ke form Grading...' ); - // TODO: Redirect ke grading form setelah submit berhasil - // router.push('/production/grading/add?recording_id=xxx'); + // Redirect ke grading form setelah submit berhasil + setTimeout(() => { + router.push('/production/recording/grading/add?recording_id=new'); + }, 1000); } }} > From 01db13ed6c4d207eb5474ed06bdda3641db71152 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 31 Oct 2025 15:15:48 +0700 Subject: [PATCH 014/101] feat(FE-170,174): add GradingForm component for managing grading records --- .../production/recording/grading/add/page.tsx | 44 + .../recording/grading/detail/edit/page.tsx | 52 ++ .../recording/grading/detail/layout.tsx | 11 + .../recording/grading/detail/page.tsx | 51 ++ .../recording/grading/form/GradingForm.tsx | 780 ++++++++++++++++++ 5 files changed, 938 insertions(+) create mode 100644 src/app/production/recording/grading/add/page.tsx create mode 100644 src/app/production/recording/grading/detail/edit/page.tsx create mode 100644 src/app/production/recording/grading/detail/layout.tsx create mode 100644 src/app/production/recording/grading/detail/page.tsx create mode 100644 src/components/pages/production/recording/grading/form/GradingForm.tsx diff --git a/src/app/production/recording/grading/add/page.tsx b/src/app/production/recording/grading/add/page.tsx new file mode 100644 index 00000000..c09a816b --- /dev/null +++ b/src/app/production/recording/grading/add/page.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; +import { RecordingApi } from '@/services/api/production'; +import { isResponseSuccess } from '@/lib/api-helper'; + +const AddGrading = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const recordingId = searchParams.get('recording_id'); + + const { data: recording, isLoading: isLoadingRecording } = useSWR( + recordingId && recordingId !== 'new' ? [recordingId] : null, + ([id]) => RecordingApi.getSingle(parseInt(id)) + ); + + if ( + recordingId && + recordingId !== 'new' && + !isLoadingRecording && + (!recording || !isResponseSuccess(recording)) + ) { + router.replace('/404'); + return; + } + + return ( +
+ {recordingId && recordingId !== 'new' && isLoadingRecording && ( + + )} + {(!recordingId || + recordingId === 'new' || + (!isLoadingRecording && recording && isResponseSuccess(recording))) && ( + + )} +
+ ); +}; + +export default AddGrading; diff --git a/src/app/production/recording/grading/detail/edit/page.tsx b/src/app/production/recording/grading/detail/edit/page.tsx new file mode 100644 index 00000000..bab82777 --- /dev/null +++ b/src/app/production/recording/grading/detail/edit/page.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; +import { RecordingApi } from '@/services/api/production'; +import { isResponseSuccess } from '@/lib/api-helper'; + +const EditGrading = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const recordingId = searchParams.get('recordingId'); + const gradingId = searchParams.get('gradingId'); + + const { data: recording, isLoading: isLoadingRecording } = useSWR( + recordingId ? [recordingId] : null, + ([id]) => RecordingApi.getSingle(parseInt(id)) + ); + + if (!recordingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingRecording && (!recording || !isResponseSuccess(recording))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingRecording && ( + + )} + {!isLoadingRecording && recording && isResponseSuccess(recording) && ( + egg.id === parseInt(gradingId || '0'))} + recordingData={recording.data} + /> + )} +
+ ); +}; + +export default EditGrading; diff --git a/src/app/production/recording/grading/detail/layout.tsx b/src/app/production/recording/grading/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/production/recording/grading/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/production/recording/grading/detail/page.tsx b/src/app/production/recording/grading/detail/page.tsx new file mode 100644 index 00000000..05901dec --- /dev/null +++ b/src/app/production/recording/grading/detail/page.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; +import GradingForm from '@/components/pages/production/recording/grading/form/GradingForm'; +import { RecordingApi } from '@/services/api/production'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const DetailGrading = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const gradingId = searchParams.get('gradingId'); + + const { data: grading, isLoading: isLoadingGrading } = useSWR( + gradingId ? [gradingId] : null, + ([id]) => RecordingApi.getSingle(parseInt(id)) + ); + + if (!gradingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingGrading && (!grading || !isResponseSuccess(grading))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingGrading && ( + + )} + {!isLoadingGrading && grading && isResponseSuccess(grading) && ( + egg.id === parseInt(gradingId))} + recordingData={grading.data} + /> + )} +
+ ); +}; + +export default DetailGrading; \ No newline at end of file diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx new file mode 100644 index 00000000..2cad7bdb --- /dev/null +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -0,0 +1,780 @@ +'use client'; + +import { useMemo, useState, useEffect, useCallback } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useFormik } from 'formik'; +import useSWR from 'swr'; +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; + +import NumberInput from '@/components/input/NumberInput'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import CheckboxInput from '@/components/input/CheckboxInput'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import { FormHeader } from '@/components/helper/form/FormHeader'; +import { RecordingApi } from '@/services/api/production'; +import { + CreateGradingPayload, + Recording, + RecordingEgg, + GradingEgg, +} from '@/types/api/production/recording'; +import { type BaseApiResponse, FormStepStatus } from '@/types/api/api-general'; +import { + RecordingGradingFormSchema, + RecordingGradingFormValues, + UpdateRecordingGradingFormSchema, + getRecordingGradingFormInitialValues, +} from '../../form/RecordingForm.schema'; +import { useRecordingFormHandlers } from '../../form/useRecordingFormHandlers'; +import { ProductWarehouseApi } from '@/services/api/inventory'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { cn } from '@/lib/helper'; +import { useModal } from '@/components/Modal'; +import toast from 'react-hot-toast'; + +import Card from '@/components/Card'; +import Badge from '@/components/Badge'; +import StepItem from '@/components/steps/StepItem'; + +interface GradingFormProps { + type?: 'add' | 'edit' | 'detail'; + initialValues?: RecordingEgg & { grading_eggs?: GradingEgg[] }; + recordingData?: Recording; +} + +const GradingForm = ({ + type = 'add', + initialValues, + recordingData, +}: GradingFormProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const recordingId = searchParams.get('recording_id'); + const [selectedGradingItems, setSelectedGradingItems] = useState( + [] + ); + + const [isApproveLoading, setIsApproveLoading] = useState(false); + const [isRejectLoading, setIsRejectLoading] = useState(false); + const [formSteps, setFormSteps] = useState(null); + + const approveModal = useModal(); + const rejectModal = useModal(); + + // ===== API DATA FETCHING ===== + const eggProductsUrl = useMemo(() => { + if (!recordingData?.project_flock_kandangs_id) return null; + const params = new URLSearchParams({ + search: '', + location_id: recordingData.project_flock_kandangs_id.toString(), + }); + return `${ProductWarehouseApi.basePath}?${params.toString()}`; + }, [recordingData]); + + const { data: eggProductsData, isLoading: isLoadingEggProducts } = useSWR( + eggProductsUrl, + ProductWarehouseApi.getAllFetcher + ); + + // ===== DATA PROCESSING ===== + const eggProducts = useMemo(() => { + const options: OptionType[] = []; + if (isResponseSuccess(eggProductsData)) { + eggProductsData.data.forEach((product) => { + const productName = product.product.name; + + if ( + productName.toLowerCase().includes('telur') || + productName.toLowerCase().includes('egg') + ) { + options.push({ + value: product.id, + label: product.product.name, + }); + } + }); + } + + return options; + }, [eggProductsData]); + + // ===== FORM HANDLERS ===== + const { + deleteModal, + recordingFormErrorMessage, + isDeleteLoading, + createRecordingHandler, + updateRecordingHandler, + deleteRecordingClickHandler, + confirmationModalDeleteClickHandler, + } = useRecordingFormHandlers(initialValues?.id); + + const formikInitialValues = useMemo(() => { + return getRecordingGradingFormInitialValues({ + recording_egg_id: initialValues?.id || parseInt(recordingId || '0') || 0, + eggs_grading: + initialValues?.grading_eggs?.map((grading: GradingEgg) => ({ + grade: grading.grade, + qty: grading.qty, + })) || [], + }); + }, [initialValues, recordingId]); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: (() => { + return type === 'edit' + ? UpdateRecordingGradingFormSchema + : RecordingGradingFormSchema; + })(), + validateOnChange: true, + validateOnBlur: true, + onSubmit: async (values) => { + const gradingPayload = { + recording_egg_id: values.recording_egg_id, + eggs_grading: (values.eggs_grading ?? []).map((grading) => ({ + grade: grading.grade, + qty: grading.qty || 0, + })), + }; + + switch (type) { + case 'add': + await createRecordingHandler(gradingPayload as CreateGradingPayload); + break; + case 'edit': + await updateRecordingHandler( + initialValues?.id as number, + gradingPayload as CreateGradingPayload + ); + break; + } + }, + }); + + // ===== EVENT HANDLERS ===== + const approveHandler = async () => { + setIsApproveLoading(true); + + const approveResponse = await RecordingApi.customRequest< + BaseApiResponse + >('approvals', { + method: 'POST', + payload: { + action: 'APPROVED', + approvable_ids: [initialValues?.id as number], + notes: 'Approved via Grading Form', + }, + }); + + if (isResponseSuccess(approveResponse)) { + toast.success('Grading berhasil disetujui!'); + approveModal.closeModal(); + router.push('/production/recording'); + } else { + toast.error( + (approveResponse?.message as string) || 'Gagal menyetujui grading' + ); + approveModal.closeModal(); + } + + setIsApproveLoading(false); + }; + + const rejectHandler = async () => { + setIsRejectLoading(true); + + const rejectResponse = await RecordingApi.customRequest< + BaseApiResponse + >('approvals', { + method: 'POST', + payload: { + action: 'REJECTED', + approvable_ids: [initialValues?.id as number], + notes: 'Rejected via Grading Form', + }, + }); + + if (isResponseSuccess(rejectResponse)) { + toast.success('Grading berhasil ditolak!'); + rejectModal.closeModal(); + router.push('/production/recording'); + } else { + toast.error( + (rejectResponse?.message as string) || 'Gagal menolak grading' + ); + rejectModal.closeModal(); + } + + setIsRejectLoading(false); + }; + + // Grading Handlers + const addGrading = () => { + const newGrading = [ + ...(formik.values.eggs_grading || []), + { + grade: '', + qty: 0, + }, + ]; + formik.setFieldValue('eggs_grading', newGrading); + }; + + const handleGradingGradeChangeWrapper = useCallback( + (idx: number) => (selectedOption: OptionType | OptionType[] | null) => { + const option = selectedOption as OptionType | null; + formik.setFieldValue(`eggs_grading.${idx}.grade`, option?.label || ''); + }, + [formik] + ); + + const handleGradingQtyChangeWrapper = useCallback( + (idx: number) => (e: React.ChangeEvent) => { + const value = parseFloat(e.target.value) || 0; + formik.setFieldValue(`eggs_grading.${idx}.qty`, value); + }, + [formik] + ); + + const removeGrading = (idx: number) => { + const updatedGrading = formik.values.eggs_grading?.filter( + (_, i) => i !== idx + ); + formik.setFieldValue('eggs_grading', updatedGrading); + }; + + const removeSelectedGrading = () => { + const updatedGrading = formik.values.eggs_grading?.filter( + (_, idx) => !selectedGradingItems.includes(idx) + ); + formik.setFieldValue('eggs_grading', updatedGrading); + setSelectedGradingItems([]); + }; + + const isRepeaterInputError = ( + arrayName: 'eggs_grading', + column: string, + idx: number + ) => { + const touched = formik.touched as Record; + const errors = formik.errors as Record; + + if (!touched[arrayName] || !Array.isArray(touched[arrayName])) { + return { + isError: false, + errorMessage: '', + }; + } + + const touchedField = (touched[arrayName] as unknown[])?.[idx] as Record< + string, + unknown + >; + const errorField = (errors[arrayName] as unknown[])?.[idx] as Record< + string, + unknown + >; + + return { + isError: touchedField && Boolean(errorField?.[column]), + errorMessage: + touchedField && errorField?.[column] + ? (errorField[column] as string) + : '', + }; + }; + + // ===== EFFECTS ===== + useEffect(() => { + const steps: FormStepStatus[] = [ + { + name: 'Recording', + isCompleted: true, + isCurrent: false, + }, + { + name: 'Grading', + isCompleted: false, + isCurrent: true, + }, + ]; + setFormSteps(steps); + }, []); + + useEffect(() => { + if (formik.values.eggs_grading && formik.values.eggs_grading.length === 0) { + formik.setFieldValue('eggs_grading', [{ grade: '', qty: 0 }]); + } + }, [formik]); + + return ( + <> +
+ + + {/* Project Flock Info Card */} +
+ {/* Form Steps */} + {formSteps && ( +
+
+
    + {formSteps.map((step, idx) => ( + 0 ? 'step-primary' : undefined + } + icon={ + step.isCompleted ? ( + + ) : ( + idx + 1 + ) + } + > + {step.name} + + ))} +
+
+ + {/* Action buttons for multi-form navigation */} + {type === 'detail' && ( +
+ +
+ )} +
+ )} +
+ + + {/* Basic Info Card */} + +
+ {type === 'detail' && recordingData ? ( + <> + Recording ID#{recordingData.id} + + ) : ( + <> + Recording Egg ID#{formik.values.recording_egg_id} + + )} +
+
+ + {/* Grading Table */} + +
+ + + + {type !== 'detail' && ( + + )} + + + {type !== 'detail' && } + + + + {formik.values.eggs_grading?.map((grading, idx) => ( + + {type !== 'detail' && ( + + )} + + + {type !== 'detail' && ( + + )} + + ))} + +
+ 0 + } + onChange={( + e: React.ChangeEvent + ) => { + if (e.target.checked) { + setSelectedGradingItems( + formik.values.eggs_grading?.map( + (_, idx) => idx + ) ?? [] + ); + } else { + setSelectedGradingItems([]); + } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> + + Grade + + * + + + Jumlah + + * + + Action
+ + ) => { + if (e.target.checked) { + setSelectedGradingItems([ + ...selectedGradingItems, + idx, + ]); + } else { + setSelectedGradingItems( + selectedGradingItems.filter((i) => i !== idx) + ); + } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> + + + + + +
+ +
+
+
+ {type !== 'detail' && ( +
+ {selectedGradingItems.length > 0 && ( + + )} + +
+ )} +
+ + {/* Action buttons */} +
+ {type !== 'add' && ( +
+ {deleteRecordingClickHandler && ( + + )} + {type !== 'edit' && initialValues && ( + + )} + {type === 'detail' && ( + <> + + + + )} +
+ )} + {type !== 'detail' && ( +
+ + +
+ )} +
+ {recordingFormErrorMessage && ( +
+ + {recordingFormErrorMessage} +
+ )} + +
+ + {type !== 'add' && ( + <> + + + {/* Approve Confirmation Modal */} + {type === 'detail' && ( + + )} + + {/* Reject Confirmation Modal */} + {type === 'detail' && ( + + )} + + )} + + ); +}; + +export default GradingForm; From 9495742cb718d384eca7526e0c07fa14385215d6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 31 Oct 2025 17:26:24 +0700 Subject: [PATCH 015/101] feat(FE-174): add grading form handlers for creating, updating, and deleting grading records --- .../grading/form/useGradingFormHandlers.ts | 88 +++++++++++++++++++ src/types/api/production/recording.d.ts | 10 +++ 2 files changed, 98 insertions(+) create mode 100644 src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts diff --git a/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts b/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts new file mode 100644 index 00000000..c24a644b --- /dev/null +++ b/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts @@ -0,0 +1,88 @@ +import { useCallback, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { toast } from 'react-hot-toast'; +import { useModal } from '@/components/Modal'; +import { RecordingApi } from '@/services/api/production'; +import { + CreateGradingPayload, + UpdateGradingPayload, +} from '@/types/api/production/recording'; +import { isResponseError } from '@/lib/api-helper'; +import { type BaseApiResponse } from '@/types/api/api-general'; + +export const useGradingFormHandlers = (gradingId?: number) => { + const router = useRouter(); + const deleteModal = useModal(); + const [recordingFormErrorMessage, setRecordingFormErrorMessage] = + useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const createGradingHandler = useCallback( + async (payload: CreateGradingPayload) => { + const res = await RecordingApi.customRequest('gradings', { + method: 'POST', + payload, + }) as BaseApiResponse; + if (isResponseError(res)) { + setRecordingFormErrorMessage(res.message); + return; + } + toast.success(res?.message || 'Successfully added Grading!'); + router.push('/production/recording'); + }, + [router] + ); + + const updateGradingHandler = useCallback( + async (gradingId: number, payload: UpdateGradingPayload) => { + const res = await RecordingApi.customRequest(`gradings/${gradingId}`, { + method: 'PUT', + payload, + }) as BaseApiResponse; + if (isResponseError(res)) { + setRecordingFormErrorMessage(res.message); + return; + } + toast.success(res?.message || 'Successfully updated Grading!'); + router.refresh(); + router.push('/production/recording'); + }, + [router] + ); + + const deleteRecordingClickHandler = useCallback(() => { + deleteModal.openModal(); + }, [deleteModal]); + + const confirmationModalDeleteClickHandler = useCallback(async () => { + if (!gradingId) return; + + setIsDeleteLoading(true); + try { + const res = await RecordingApi.customRequest(`gradings/${gradingId}`, { + method: 'DELETE', + }) as BaseApiResponse; + if (isResponseError(res)) { + setRecordingFormErrorMessage(res.message); + return; + } + deleteModal.closeModal(); + toast.success(res?.message || 'Successfully delete Grading!'); + router.push('/production/recording'); + } catch (error) { + setRecordingFormErrorMessage('Failed to delete Grading'); + } finally { + setIsDeleteLoading(false); + } + }, [deleteModal, gradingId, router]); + + return { + deleteModal, + recordingFormErrorMessage, + isDeleteLoading, + createGradingHandler, + updateGradingHandler, + deleteRecordingClickHandler, + confirmationModalDeleteClickHandler, + }; +}; \ No newline at end of file diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 55272533..171ba09e 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -93,6 +93,16 @@ export type CreateGradingPayload = { }[]; }; +export type UpdateGradingPayload = CreateGradingPayload; + +export type CreateGradingRecordingPayload = { + recording_egg_id: number; + eggs_grading: { + grade: string; + qty: number; + }[]; +}; + export type CreateEggPayload = { product_warehouse_id: number; qty: number; From 19afb80597ef928033ce25ab4f329ef013bbde70 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 31 Oct 2025 17:26:56 +0700 Subject: [PATCH 016/101] feat(FE-170,174): refactor GradingForm to use grading form handlers and remove approval logic --- .../production/recording/grading/add/page.tsx | 2 +- .../recording/grading/detail/edit/page.tsx | 5 +- .../recording/grading/detail/page.tsx | 7 +- .../recording/grading/{detail => }/layout.tsx | 0 .../recording/grading/form/GradingForm.tsx | 220 +++--------------- 5 files changed, 43 insertions(+), 191 deletions(-) rename src/app/production/recording/grading/{detail => }/layout.tsx (100%) diff --git a/src/app/production/recording/grading/add/page.tsx b/src/app/production/recording/grading/add/page.tsx index c09a816b..2c1859cb 100644 --- a/src/app/production/recording/grading/add/page.tsx +++ b/src/app/production/recording/grading/add/page.tsx @@ -35,7 +35,7 @@ const AddGrading = () => { {(!recordingId || recordingId === 'new' || (!isLoadingRecording && recording && isResponseSuccess(recording))) && ( - + )}
); diff --git a/src/app/production/recording/grading/detail/edit/page.tsx b/src/app/production/recording/grading/detail/edit/page.tsx index bab82777..4403e04a 100644 --- a/src/app/production/recording/grading/detail/edit/page.tsx +++ b/src/app/production/recording/grading/detail/edit/page.tsx @@ -41,8 +41,9 @@ const EditGrading = () => { {!isLoadingRecording && recording && isResponseSuccess(recording) && ( egg.id === parseInt(gradingId || '0'))} - recordingData={recording.data} + initialValues={recording.data.recording_eggs?.find( + (egg) => egg.id === parseInt(gradingId || '0') + )} /> )}
diff --git a/src/app/production/recording/grading/detail/page.tsx b/src/app/production/recording/grading/detail/page.tsx index 05901dec..afe1a5bc 100644 --- a/src/app/production/recording/grading/detail/page.tsx +++ b/src/app/production/recording/grading/detail/page.tsx @@ -40,12 +40,13 @@ const DetailGrading = () => { {!isLoadingGrading && grading && isResponseSuccess(grading) && ( egg.id === parseInt(gradingId))} - recordingData={grading.data} + initialValues={grading.data.recording_eggs?.find( + (egg) => egg.id === parseInt(gradingId) + )} /> )}
); }; -export default DetailGrading; \ No newline at end of file +export default DetailGrading; diff --git a/src/app/production/recording/grading/detail/layout.tsx b/src/app/production/recording/grading/layout.tsx similarity index 100% rename from src/app/production/recording/grading/detail/layout.tsx rename to src/app/production/recording/grading/layout.tsx diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 2cad7bdb..d07e61b1 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -15,39 +15,31 @@ import { FormHeader } from '@/components/helper/form/FormHeader'; import { RecordingApi } from '@/services/api/production'; import { CreateGradingPayload, - Recording, + UpdateGradingPayload, RecordingEgg, GradingEgg, } from '@/types/api/production/recording'; -import { type BaseApiResponse, FormStepStatus } from '@/types/api/api-general'; +import { type FormStepStatus } from '@/types/api/api-general'; import { RecordingGradingFormSchema, RecordingGradingFormValues, UpdateRecordingGradingFormSchema, getRecordingGradingFormInitialValues, } from '../../form/RecordingForm.schema'; -import { useRecordingFormHandlers } from '../../form/useRecordingFormHandlers'; -import { ProductWarehouseApi } from '@/services/api/inventory'; -import { isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; -import { useModal } from '@/components/Modal'; import toast from 'react-hot-toast'; import Card from '@/components/Card'; -import Badge from '@/components/Badge'; import StepItem from '@/components/steps/StepItem'; +import { useModal } from '@/components/Modal'; +import { useGradingFormHandlers } from './useGradingFormHandlers'; interface GradingFormProps { type?: 'add' | 'edit' | 'detail'; initialValues?: RecordingEgg & { grading_eggs?: GradingEgg[] }; - recordingData?: Recording; } -const GradingForm = ({ - type = 'add', - initialValues, - recordingData, -}: GradingFormProps) => { +const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { const router = useRouter(); const searchParams = useSearchParams(); const recordingId = searchParams.get('recording_id'); @@ -55,64 +47,45 @@ const GradingForm = ({ [] ); - const [isApproveLoading, setIsApproveLoading] = useState(false); - const [isRejectLoading, setIsRejectLoading] = useState(false); const [formSteps, setFormSteps] = useState(null); - const approveModal = useModal(); - const rejectModal = useModal(); - // ===== API DATA FETCHING ===== - const eggProductsUrl = useMemo(() => { - if (!recordingData?.project_flock_kandangs_id) return null; - const params = new URLSearchParams({ - search: '', - location_id: recordingData.project_flock_kandangs_id.toString(), - }); - return `${ProductWarehouseApi.basePath}?${params.toString()}`; - }, [recordingData]); + // Fetch existing gradings for the recording egg to show recorded data + // Optional: Fetch existing gradings if needed for display purposes + // const existingGradingsUrl = useMemo(() => { + // const recordingEggIdToUse = recordingId || initialValues?.id?.toString(); + // if (!recordingEggIdToUse) return null; + // return `${RecordingApi.basePath}/gradings?recording_egg_id=${recordingEggIdToUse}`; + // }, [recordingId, initialValues]); - const { data: eggProductsData, isLoading: isLoadingEggProducts } = useSWR( - eggProductsUrl, - ProductWarehouseApi.getAllFetcher - ); + // const { data: existingGradings } = useSWR( + // existingGradingsUrl, + // existingGradingsUrl ? RecordingApi.getAllFetcher : null + // ); // ===== DATA PROCESSING ===== - const eggProducts = useMemo(() => { - const options: OptionType[] = []; - if (isResponseSuccess(eggProductsData)) { - eggProductsData.data.forEach((product) => { - const productName = product.product.name; - - if ( - productName.toLowerCase().includes('telur') || - productName.toLowerCase().includes('egg') - ) { - options.push({ - value: product.id, - label: product.product.name, - }); - } - }); - } - - return options; - }, [eggProductsData]); + // No data processing needed - grading form only needs recording_egg_id and grading data // ===== FORM HANDLERS ===== const { deleteModal, recordingFormErrorMessage, isDeleteLoading, - createRecordingHandler, - updateRecordingHandler, + createGradingHandler, + updateGradingHandler, deleteRecordingClickHandler, confirmationModalDeleteClickHandler, - } = useRecordingFormHandlers(initialValues?.id); + } = useGradingFormHandlers(initialValues?.id); const formikInitialValues = useMemo(() => { + let recordingEggId: number | undefined = initialValues?.id; + + if (!recordingEggId) { + recordingEggId = parseInt(recordingId || '0') || 0; + } + return getRecordingGradingFormInitialValues({ - recording_egg_id: initialValues?.id || parseInt(recordingId || '0') || 0, + recording_egg_id: recordingEggId, eggs_grading: initialValues?.grading_eggs?.map((grading: GradingEgg) => ({ grade: grading.grade, @@ -141,74 +114,19 @@ const GradingForm = ({ switch (type) { case 'add': - await createRecordingHandler(gradingPayload as CreateGradingPayload); + await createGradingHandler(gradingPayload as CreateGradingPayload); break; case 'edit': - await updateRecordingHandler( + await updateGradingHandler( initialValues?.id as number, - gradingPayload as CreateGradingPayload + gradingPayload as UpdateGradingPayload ); break; } }, }); - // ===== EVENT HANDLERS ===== - const approveHandler = async () => { - setIsApproveLoading(true); - - const approveResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'APPROVED', - approvable_ids: [initialValues?.id as number], - notes: 'Approved via Grading Form', - }, - }); - - if (isResponseSuccess(approveResponse)) { - toast.success('Grading berhasil disetujui!'); - approveModal.closeModal(); - router.push('/production/recording'); - } else { - toast.error( - (approveResponse?.message as string) || 'Gagal menyetujui grading' - ); - approveModal.closeModal(); - } - - setIsApproveLoading(false); - }; - - const rejectHandler = async () => { - setIsRejectLoading(true); - - const rejectResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'REJECTED', - approvable_ids: [initialValues?.id as number], - notes: 'Rejected via Grading Form', - }, - }); - - if (isResponseSuccess(rejectResponse)) { - toast.success('Grading berhasil ditolak!'); - rejectModal.closeModal(); - router.push('/production/recording'); - } else { - toast.error( - (rejectResponse?.message as string) || 'Gagal menolak grading' - ); - rejectModal.closeModal(); - } - - setIsRejectLoading(false); - }; + // Grading form doesn't need approval/reject handlers // Grading Handlers const addGrading = () => { @@ -309,6 +227,8 @@ const GradingForm = ({ } }, [formik]); + // Grading form doesn't need loading state since it only depends on recording_egg_id + return ( <>
@@ -394,9 +314,9 @@ const GradingForm = ({ : 'grid grid-cols-3 gap-4' } > - {type === 'detail' && recordingData ? ( + {type === 'detail' && initialValues ? ( <> - Recording ID#{recordingData.id} + Recording Egg ID#{initialValues.id} ) : ( <> @@ -641,40 +561,6 @@ const GradingForm = ({ Edit )} - {type === 'detail' && ( - <> - - - - )} )} {type !== 'detail' && ( @@ -735,42 +621,6 @@ const GradingForm = ({ onClick: confirmationModalDeleteClickHandler, }} /> - - {/* Approve Confirmation Modal */} - {type === 'detail' && ( - - )} - - {/* Reject Confirmation Modal */} - {type === 'detail' && ( - - )} )} From 1228b4504511d74636aa2c48024d9699a78d80aa Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 31 Oct 2025 17:27:10 +0700 Subject: [PATCH 017/101] feat(FE-170,174): clean up RecordingForm and RecordingTable components for improved readability and maintainability --- .../production/recording/RecordingTable.tsx | 142 ++++++++++++------ .../recording/form/RecordingForm.tsx | 20 +-- 2 files changed, 105 insertions(+), 57 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 413408ab..5e018e61 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -112,7 +112,9 @@ const RecordingTable = () => { const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); - const [selectedRecording, setSelectedRecording] = useState(undefined); + const [selectedRecording, setSelectedRecording] = useState< + Recording | undefined + >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false); const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false); @@ -127,8 +129,12 @@ const RecordingTable = () => { const [kandangSelectInputValue, setKandangSelectInputValue] = useState(''); const [selectedArea, setSelectedArea] = useState(null); - const [selectedLocation, setSelectedLocation] = useState(null); - const [selectedKandang, setSelectedKandang] = useState(null); + const [selectedLocation, setSelectedLocation] = useState( + null + ); + const [selectedKandang, setSelectedKandang] = useState( + null + ); const { data: recordings, @@ -144,20 +150,20 @@ const RecordingTable = () => { search: areaSelectInputValue, limit: '100', }).toString()}`; - const { - data: areas, - isLoading: isLoadingAreas, - } = useSWR(areaUrl, AreaApi.getAllFetcher); + const { data: areas, isLoading: isLoadingAreas } = useSWR( + areaUrl, + AreaApi.getAllFetcher + ); const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({ search: locationSelectInputValue, area_id: selectedArea != null ? selectedArea.value.toString() : '', limit: '100', }).toString()}`; - const { - data: locations, - isLoading: isLoadingLocations, - } = useSWR(locationUrl, LocationApi.getAllFetcher); + const { data: locations, isLoading: isLoadingLocations } = useSWR( + locationUrl, + LocationApi.getAllFetcher + ); const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({ search: kandangSelectInputValue, @@ -165,10 +171,10 @@ const RecordingTable = () => { selectedLocation != null ? selectedLocation.value.toString() : '', limit: '100', }).toString()}`; - const { - data: kandangs, - isLoading: isLoadingKandang, - } = useSWR(kandangUrl, KandangApi.getAllFetcher); + const { data: kandangs, isLoading: isLoadingKandang } = useSWR( + kandangUrl, + KandangApi.getAllFetcher + ); // Data to Options Mapping const optionsArea = isResponseSuccess(areas) @@ -213,7 +219,9 @@ const RecordingTable = () => { return recordings.data; }, [recordings]); - const selectedRowIds = Object.keys(rowSelection).map((item) => parseInt(item)); + const selectedRowIds = Object.keys(rowSelection).map((item) => + parseInt(item) + ); const bulkApproveHandler = async () => { setIsBulkApproveLoading(true); @@ -233,7 +241,9 @@ const RecordingTable = () => { await refreshRecordings(); setRowSelection({}); bulkApproveModal.closeModal(); - toast.success(`Successfully approved ${selectedRowIds.length} recordings!`); + toast.success( + `Successfully approved ${selectedRowIds.length} recordings!` + ); } if (isResponseError(approveResponse)) { toast.error(approveResponse?.message as string); @@ -260,7 +270,9 @@ const RecordingTable = () => { refreshRecordings(); setRowSelection({}); bulkRejectModal.closeModal(); - toast.success(`Successfully rejected ${selectedRowIds.length} recordings!`); + toast.success( + `Successfully rejected ${selectedRowIds.length} recordings!` + ); } if (isResponseError(rejectResponse)) { toast.error(rejectResponse?.message as string); @@ -312,7 +324,10 @@ const RecordingTable = () => { setSelectedArea(selectedValue); setSelectedLocation(null); setSelectedKandang(null); - updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter( + 'areaFilter', + selectedValue ? selectedValue.value.toString() : '' + ); updateFilter('locationFilter', ''); updateFilter('kandangFilter', ''); setPage(1); @@ -332,7 +347,10 @@ const RecordingTable = () => { const selectedValue = selected as OptionType | null; setSelectedLocation(selectedValue); setSelectedKandang(null); - updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter( + 'locationFilter', + selectedValue ? selectedValue.value.toString() : '' + ); updateFilter('kandangFilter', ''); setPage(1); }} @@ -351,7 +369,10 @@ const RecordingTable = () => { onChange={(selected) => { const selectedValue = selected as OptionType | null; setSelectedKandang(selectedValue); - updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter( + 'kandangFilter', + selectedValue ? selectedValue.value.toString() : '' + ); setPage(1); }} className={{ wrapper: 'w-full' }} @@ -371,12 +392,18 @@ const RecordingTable = () => { ]} value={ tableFilterState.periodFilter - ? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` } + ? { + value: tableFilterState.periodFilter, + label: `Periode ${tableFilterState.periodFilter}`, + } : null } onChange={(selected) => { const selectedValue = selected as OptionType | null; - updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter( + 'periodFilter', + selectedValue ? selectedValue.value.toString() : '' + ); setPage(1); }} className={{ wrapper: 'w-full' }} @@ -396,7 +423,10 @@ const RecordingTable = () => { setSelectedArea(selectedValue); setSelectedLocation(null); setSelectedKandang(null); - updateFilter('areaFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter( + 'areaFilter', + selectedValue ? selectedValue.value.toString() : '' + ); updateFilter('locationFilter', ''); updateFilter('kandangFilter', ''); setPage(1); @@ -416,7 +446,10 @@ const RecordingTable = () => { const selectedValue = selected as OptionType | null; setSelectedLocation(selectedValue); setSelectedKandang(null); - updateFilter('locationFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter( + 'locationFilter', + selectedValue ? selectedValue.value.toString() : '' + ); updateFilter('kandangFilter', ''); setPage(1); }} @@ -435,7 +468,10 @@ const RecordingTable = () => { onChange={(selected) => { const selectedValue = selected as OptionType | null; setSelectedKandang(selectedValue); - updateFilter('kandangFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter( + 'kandangFilter', + selectedValue ? selectedValue.value.toString() : '' + ); setPage(1); }} className={{ wrapper: 'w-full' }} @@ -455,12 +491,18 @@ const RecordingTable = () => { ]} value={ tableFilterState.periodFilter - ? { value: tableFilterState.periodFilter, label: `Periode ${tableFilterState.periodFilter}` } + ? { + value: tableFilterState.periodFilter, + label: `Periode ${tableFilterState.periodFilter}`, + } : null } onChange={(selected) => { const selectedValue = selected as OptionType | null; - updateFilter('periodFilter', selectedValue ? selectedValue.value.toString() : ''); + updateFilter( + 'periodFilter', + selectedValue ? selectedValue.value.toString() : '' + ); setPage(1); }} className={{ wrapper: 'w-full' }} @@ -562,11 +604,15 @@ const RecordingTable = () => { }, { header: '#', - cell: (props) => tableFilterState.pageSize * (tableFilterState.page - 1) + props.row.index + 1, + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, }, { header: 'Nama Project', - cell: (props) => `Project ${props.row.original.project_flock_kandang_id}`, + cell: (props) => + `Project ${props.row.original.project_flock_kandangs_id}`, }, { header: 'Umur (hari)', @@ -576,19 +622,22 @@ const RecordingTable = () => { accessorKey: 'record_date', header: 'Waktu Recording', cell: (props) => - new Date(props.row.original.record_date).toLocaleDateString(), + new Date(props.row.original.record_datetime).toLocaleDateString(), }, { header: 'Populasi Awal', - cell: (props) => props.row.original.total_chick?.toLocaleString() || '-', + cell: (props) => + props.row.original.total_chick_qty?.toLocaleString() || '-', }, { header: 'BW', - cell: (props) => props.row.original.avg_daily_gain?.toFixed(2) || '-', + cell: (props) => + props.row.original.avg_daily_gain?.toFixed(2) || '-', }, { header: 'Pakan', - cell: (props) => props.row.original.cum_intake?.toLocaleString() || '-', + cell: (props) => + props.row.original.cum_intake?.toLocaleString() || '-', }, { header: 'FCR', @@ -597,19 +646,20 @@ const RecordingTable = () => { { accessorKey: 'total_depletion', header: 'Total Deplesi', - cell: (props) => props.row.original.total_depletion, + cell: (props) => props.row.original.total_depletion_qty, }, { header: 'Deplesi (%)', - cell: (props) => props.row.original.daily_depletion_rate?.toFixed(2) || '-', + cell: (props) => + props.row.original.daily_depletion_rate?.toFixed(2) || '-', }, { header: 'Populasi Akhir', - cell: (props) => (props.row.original.total_chick - props.row.original.total_depletion)?.toLocaleString() || '-', - }, - { - header: 'Ketepatan Waktu', - cell: (props) => props.row.original.ontime ? 'Tepat Waktu' : 'Terlambat', + cell: (props) => + ( + props.row.original.total_chick_qty - + props.row.original.total_depletion_qty + )?.toLocaleString() || '-', }, { header: 'Tanggal Submit', @@ -660,8 +710,14 @@ const RecordingTable = () => { }, ]} pageSize={tableFilterState.pageSize} - page={recordings?.status === 'success' ? recordings.meta?.page : tableFilterState.page} - totalItems={recordings?.status === 'success' ? recordings.meta?.total_results : 0} + page={ + recordings?.status === 'success' + ? recordings.meta?.page + : tableFilterState.page + } + totalItems={ + recordings?.status === 'success' ? recordings.meta?.total_results : 0 + } onPageChange={setPage} isLoading={isLoading} sorting={sorting} diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index f024c3bf..5dfddbdd 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -43,7 +43,6 @@ import toast from 'react-hot-toast'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; -import Steps from '@/components/steps/Steps'; import StepItem from '@/components/steps/StepItem'; import { Kandang } from '@/types/api/master-data/kandang'; @@ -1038,17 +1037,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Project Flock Info Card */} {projectFlockKandangLookup && (
- - {projectFlockKandangLookup.project_flock.category} - - {/* Form Steps for LAYING Category */} {formSteps && (
@@ -1089,7 +1077,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { type='button' color='primary' onClick={() => { - router.push(`/production/recording/grading/add?recording_id=${initialValues?.id}`); + router.push( + `/production/recording/grading/add?recording_id=${initialValues?.id}` + ); }} > Lanjut ke Grading @@ -2203,7 +2193,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ); // Redirect ke grading form setelah submit berhasil setTimeout(() => { - router.push('/production/recording/grading/add?recording_id=new'); + router.push( + '/production/recording/grading/add?recording_id=new' + ); }, 1000); } }} From e116311dc2d60bc8b0e79bf85c588b87d2943f47 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 2 Nov 2025 23:04:54 +0700 Subject: [PATCH 018/101] refactor(FE-170,174): restructure schema and validation for body weights, stocks, and depletions; improve handling of input values --- .../recording/form/RecordingForm.schema.ts | 307 +++++++++++------- .../recording/form/RecordingForm.tsx | 44 +-- 2 files changed, 207 insertions(+), 144 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index fb58b048..5bd914b2 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -7,107 +7,165 @@ import { CreateGradingPayload, } from '@/types/api/production/recording'; -export const RecordingGrowingFormSchema = Yup.object({ - project_flock_kandang: Yup.object({ - value: Yup.number().min(1).required(), - label: Yup.string().required(), - }).nullable(), - project_flock_kandangs_id: Yup.number() - .default(0) - .typeError('Project Flock Kandang wajib diisi!') - .test( - 'is-valid-project-flock-kandang', - 'Project Flock Kandang wajib diisi!', - (value) => value !== undefined && value !== null && value > 0 - ) - .required('Project Flock Kandang wajib diisi!') - .test( - 'not-already-recorded', - 'Project Flock ini sudah direcord hari ini!', - function (value) { - const recordedProjectFlockIds = this.options.context - ?.recordedProjectFlockIds as Set; - const formType = this.options.context?.type as - | 'add' - | 'edit' - | 'detail'; - if (formType !== 'add') return true; - if (value && recordedProjectFlockIds?.has(value)) { - return false; - } - return true; - } - ), - body_weights: Yup.array() - .of( - Yup.object({ - weight: Yup.number() - .required('Berat ayam total wajib diisi!') - .min(1, 'Berat ayam total minimal 1 gram!') - .typeError('Berat ayam total harus berupa angka!'), - avg_weight: Yup.number() - .required('Berat ayam rata-rata wajib diisi!') - .min(1, 'Berat ayam rata-rata minimal 1 gram!') - .typeError('Berat ayam rata-rata harus berupa angka!'), - qty: Yup.number() - .required('Jumlah ayam wajib diisi!') - .min(1, 'Jumlah ayam minimal 1 ekor!') - .typeError('Jumlah ayam harus berupa angka!') - .default(1), - }) - ) - .min(1, 'Minimal harus ada 1 data bobot badan!') - .required('Data bobot badan wajib diisi!'), - stocks: Yup.array() - .of( - Yup.object({ - product_warehouse_id: Yup.number() - .required('Produk wajib diisi!') - .min(1, 'Produk wajib diisi!') - .typeError('Produk harus berupa angka!'), - usage_qty: Yup.number() - .required('Jumlah penggunaan wajib diisi!') - .min(1, 'Jumlah penggunaan tidak boleh 0!') - .typeError('Jumlah penggunaan harus berupa angka!'), - }) - ) - .min(1, 'Minimal harus ada 1 data stok!') - .required('Data stok wajib diisi!'), - depletions: Yup.array() - .of( - Yup.object({ - product_warehouse_id: Yup.number() - .required('Produk depletions wajib diisi!') - .min(1, 'Produk depletions wajib diisi!') - .typeError('Produk depletions harus berupa angka!'), - qty: Yup.number() - .required('Jumlah depletions wajib diisi!') - .min(1, 'Jumlah depletions minimal 1!') - .typeError('Jumlah depletions harus berupa angka!'), - }) - ) - .min(1, 'Minimal harus ada 1 data depletions!') - .required('Data depletions wajib diisi!'), +type RecordingGrowingFormSchemaType = { + project_flock_kandang: { + value: number; + label: string; + } | null; + project_flock_kandangs_id: number; + body_weights: { + weight: number | string; + avg_weight: number | string; + qty: number | string; + }[]; + stocks: { + product_warehouse_id: number; + usage_qty: number | string; + }[]; + depletions: { + product_warehouse_id: number; + qty: number | string; + }[]; +}; + +type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & { + eggs: { + product_warehouse_id: number; + qty: number | string; + }[]; +}; + +type RecordingGradingFormSchemaType = { + recording_egg_id: number; + eggs_grading: { + grade: string; + qty: number | string; + }[]; +}; + +export type BodyWeightSchema = { + weight: number | string; + avg_weight: number | string; + qty: number | string; +}; + +export type StockSchema = { + product_warehouse_id: number; + usage_qty: number | string; +}; + +export type DepletionSchema = { + product_warehouse_id: number; + qty: number | string; +}; + +export type EggSchema = { + product_warehouse_id: number; + qty: number | string; +}; + +const BodyWeightObjectSchema: Yup.ObjectSchema = Yup.object({ + weight: Yup.number() + .required('Berat ayam total wajib diisi!') + .min(1, 'Berat ayam total minimal 1 gram!') + .typeError('Berat ayam total harus berupa angka!'), + avg_weight: Yup.number() + .required('Berat ayam rata-rata wajib diisi!') + .min(1, 'Berat ayam rata-rata minimal 1 gram!') + .typeError('Berat ayam rata-rata harus berupa angka!'), + qty: Yup.number() + .required('Jumlah ayam wajib diisi!') + .min(1, 'Jumlah ayam minimal 1 ekor!') + .typeError('Jumlah ayam harus berupa angka!'), }); -export const RecordingLayingFormSchema = RecordingGrowingFormSchema.shape({ - eggs: Yup.array() - .of( - Yup.object({ - product_warehouse_id: Yup.number() - .required('Produk telur wajib diisi!') - .min(1, 'Produk telur wajib diisi!') - .typeError('Produk telur harus berupa angka!'), - qty: Yup.number() - .required('Jumlah telur wajib diisi!') - .min(1, 'Jumlah telur tidak boleh 0!') - .typeError('Jumlah telur harus berupa angka!'), - }) - ) - .min(1, 'Minimal harus ada 1 data telur!') - .required('Data telur wajib diisi!'), +const StockObjectSchema: Yup.ObjectSchema = Yup.object({ + product_warehouse_id: Yup.number() + .required('Produk wajib diisi!') + .min(1, 'Produk wajib diisi!') + .typeError('Produk harus berupa angka!'), + usage_qty: Yup.number() + .required('Jumlah penggunaan wajib diisi!') + .min(1, 'Jumlah penggunaan tidak boleh 0!') + .typeError('Jumlah penggunaan harus berupa angka!'), }); +const DepletionObjectSchema: Yup.ObjectSchema = Yup.object({ + product_warehouse_id: Yup.number() + .required('Produk depletions wajib diisi!') + .min(1, 'Produk depletions wajib diisi!') + .typeError('Produk depletions harus berupa angka!'), + qty: Yup.number() + .required('Jumlah depletions wajib diisi!') + .min(1, 'Jumlah depletions minimal 1!') + .typeError('Jumlah depletions harus berupa angka!'), +}); + +const EggObjectSchema: Yup.ObjectSchema = Yup.object({ + product_warehouse_id: Yup.number() + .required('Produk telur wajib diisi!') + .min(1, 'Produk telur wajib diisi!') + .typeError('Produk telur harus berupa angka!'), + qty: Yup.number() + .required('Jumlah telur wajib diisi!') + .min(1, 'Jumlah telur tidak boleh 0!') + .typeError('Jumlah telur harus berupa angka!'), +}); + +export const RecordingGrowingFormSchema: Yup.ObjectSchema = + Yup.object({ + project_flock_kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_kandangs_id: Yup.number() + .default(0) + .typeError('Project Flock Kandang wajib diisi!') + .test( + 'is-valid-project-flock-kandang', + 'Project Flock Kandang wajib diisi!', + (value) => value !== undefined && value !== null && value > 0 + ) + .required('Project Flock Kandang wajib diisi!') + .test( + 'not-already-recorded', + 'Project Flock ini sudah direcord hari ini!', + function (value) { + const recordedProjectFlockIds = this.options.context + ?.recordedProjectFlockIds as Set; + const formType = this.options.context?.type as + | 'add' + | 'edit' + | 'detail'; + if (formType !== 'add') return true; + if (value && recordedProjectFlockIds?.has(value)) { + return false; + } + return true; + } + ), + body_weights: Yup.array() + .of(BodyWeightObjectSchema) + .min(1, 'Minimal harus ada 1 data bobot badan!') + .required('Data bobot badan wajib diisi!'), + stocks: Yup.array() + .of(StockObjectSchema) + .min(1, 'Minimal harus ada 1 data stok!') + .required('Data stok wajib diisi!'), + depletions: Yup.array() + .of(DepletionObjectSchema) + .min(1, 'Minimal harus ada 1 data depletions!') + .required('Data depletions wajib diisi!'), + }); + +export const RecordingLayingFormSchema: Yup.ObjectSchema = + RecordingGrowingFormSchema.shape({ + eggs: Yup.array() + .of(EggObjectSchema) + .min(1, 'Minimal harus ada 1 data telur!') + .required('Data telur wajib diisi!'), + }); + export const UpdateRecordingGrowingFormSchema = RecordingGrowingFormSchema.shape({ project_flock_kandangs_id: Yup.number() @@ -133,26 +191,27 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({ .required('Project Flock Kandang wajib diisi!'), }); -export const RecordingGradingFormSchema = Yup.object({ - recording_egg_id: Yup.number() - .required('Recording Egg ID wajib diisi!') - .min(1, 'Recording Egg ID minimal 1!') - .typeError('Recording Egg ID harus berupa angka!'), - eggs_grading: Yup.array() - .of( - Yup.object({ - grade: Yup.string() - .required('Grade telur wajib diisi!') - .typeError('Grade telur harus berupa string!'), - qty: Yup.number() - .required('Jumlah telur wajib diisi!') - .min(1, 'Jumlah telur minimal 1!') - .typeError('Jumlah telur harus berupa angka!'), - }) - ) - .min(1, 'Minimal harus ada 1 data grading telur!') - .required('Data grading telur wajib diisi!'), -}); +export const RecordingGradingFormSchema: Yup.ObjectSchema = + Yup.object({ + recording_egg_id: Yup.number() + .required('Recording Egg ID wajib diisi!') + .min(1, 'Recording Egg ID minimal 1!') + .typeError('Recording Egg ID harus berupa angka!'), + eggs_grading: Yup.array() + .of( + Yup.object({ + grade: Yup.string() + .required('Grade telur wajib diisi!') + .typeError('Grade telur harus berupa string!'), + qty: Yup.number() + .required('Jumlah telur wajib diisi!') + .min(1, 'Jumlah telur minimal 1!') + .typeError('Jumlah telur harus berupa angka!'), + }) + ) + .min(1, 'Minimal harus ada 1 data grading telur!') + .required('Data grading telur wajib diisi!'), + }); export const UpdateRecordingGradingFormSchema = RecordingGradingFormSchema.shape({ @@ -199,9 +258,9 @@ export const getRecordingGrowingFormInitialValues = ( }) ) ?? [ { - weight: 0, - avg_weight: 0, - qty: 0, + weight: '', + avg_weight: '', + qty: '', }, ], stocks: initialValues?.stocks?.map( @@ -212,7 +271,7 @@ export const getRecordingGrowingFormInitialValues = ( ) ?? [ { product_warehouse_id: 0, - usage_qty: 0, + usage_qty: '', }, ], depletions: initialValues?.depletions?.map( @@ -225,7 +284,7 @@ export const getRecordingGrowingFormInitialValues = ( ) ?? [ { product_warehouse_id: 0, - qty: 0, + qty: '', }, ], }); @@ -241,7 +300,7 @@ export const getRecordingLayingFormInitialValues = ( })) ?? [ { product_warehouse_id: 0, - qty: 0, + qty: '', }, ], }); @@ -256,7 +315,7 @@ export const getRecordingGradingFormInitialValues = ( })) ?? [ { grade: '', - qty: 0, + qty: '', }, ], }); diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 5dfddbdd..020de967 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -773,7 +773,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const currentWeight = formik.values.body_weights?.[idx]; if (currentWeight) { - const qty = currentWeight.qty; + const qty = Number(currentWeight.qty) || 0; if (qty > 0 && value > 0) { const avgWeight = parseFloat((value / qty).toFixed(2)); formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight); @@ -794,7 +794,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const currentWeight = formik.values.body_weights?.[idx]; if (currentWeight) { - const qty = currentWeight.qty; + const qty = Number(currentWeight.qty) || 0; if (qty > 0 && value > 0) { const totalWeight = value * qty; formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight); @@ -815,7 +815,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const currentWeight = formik.values.body_weights?.[idx]; if (currentWeight) { - const weight = currentWeight.weight; + const weight = Number(currentWeight.weight) || 0; if (value > 0 && weight > 0) { const avgWeight = parseFloat((weight / value).toFixed(2)); formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight); @@ -864,7 +864,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ...(formik.values.stocks || []), { product_warehouse_id: 0, - usage_qty: 0, + usage_qty: '', }, ]; formik.setFieldValue('stocks', newStocks); @@ -897,7 +897,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ...(formik.values.depletions || []), { product_warehouse_id: 0, - qty: 0, + qty: '', }, ]; formik.setFieldValue('depletions', newDepletions); @@ -932,7 +932,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ...((formik.values as RecordingLayingFormValues).eggs || []), { product_warehouse_id: 0, - qty: 0, + qty: '', }, ]; formik.setFieldValue('eggs', newEggs); @@ -966,7 +966,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (isLayingCategory && (type as 'add' | 'edit' | 'detail') !== 'detail') { const layingValues = formik.values as RecordingLayingFormValues; if (!layingValues.eggs || layingValues.eggs.length === 0) { - formik.setFieldValue('eggs', [{ product_warehouse_id: 0, qty: 0 }]); + formik.setFieldValue('eggs', [{ product_warehouse_id: 0, qty: '' }]); } } }, [isLayingCategory, type, formik]); @@ -998,11 +998,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (idx === editingAverageIndex || manuallyEditedRows.has(idx)) { return weight; } + const qty = Number(weight.qty) || 0; + const weightValue = Number(weight.weight) || 0; return { ...weight, avg_weight: - weight.qty > 0 && weight.weight > 0 - ? parseFloat((weight.weight / weight.qty).toFixed(2)) + qty > 0 && weightValue > 0 + ? parseFloat((weightValue / qty).toFixed(2)) : 0, }; } @@ -1303,7 +1305,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { { thousandSeparator=',' decimalSeparator='.' inputSuffix='gram' + placeholder='Masukkan berat total...' isError={ isRepeaterInputError('body_weights', 'weight', idx) .isError @@ -1329,13 +1332,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { { { thousandSeparator=',' decimalSeparator='.' inputSuffix='gram' + placeholder='Masukkan berat rata-rata...' isError={ isRepeaterInputError( 'body_weights', @@ -1574,7 +1579,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { { className={{ wrapper: 'w-full min-w-24', }} - placeholder='Jumlah Pakai' + placeholder='Masukkan jumlah pakai' /> {(type as 'add' | 'edit' | 'detail') !== 'detail' && getStockUsageAdornment(idx)} @@ -1787,7 +1792,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { { className={{ wrapper: 'w-full min-w-24', }} - placeholder='Jumlah' + placeholder='Masukkan jumlah deplesi' /> {(type as 'add' | 'edit' | 'detail') !== 'detail' && ( @@ -1998,7 +2003,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { { className={{ wrapper: 'w-full min-w-24', }} - placeholder='Jumlah' + placeholder='Masukkan jumlah telur' />
@@ -2191,10 +2196,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { toast.success( 'Recording berhasil disimpan! Mengalihkan ke form Grading...' ); - // Redirect ke grading form setelah submit berhasil setTimeout(() => { router.push( - '/production/recording/grading/add?recording_id=new' + '/production/recording/grading/add?recording_id=1' ); }, 1000); } From aac7215be7a701da99b9cf82f2c2e1159a4039b4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 2 Nov 2025 23:14:07 +0700 Subject: [PATCH 019/101] refactor(FE-170,174): update schema and validation for stocks and depletions; rename usage_qty to qty for consistency --- .../recording/form/RecordingForm.schema.ts | 10 +++--- .../recording/form/RecordingForm.tsx | 34 +++++++++---------- src/types/api/production/recording.d.ts | 5 ++- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 5bd914b2..f91a4e7b 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -20,7 +20,7 @@ type RecordingGrowingFormSchemaType = { }[]; stocks: { product_warehouse_id: number; - usage_qty: number | string; + qty: number | string; }[]; depletions: { product_warehouse_id: number; @@ -51,7 +51,7 @@ export type BodyWeightSchema = { export type StockSchema = { product_warehouse_id: number; - usage_qty: number | string; + qty: number | string; }; export type DepletionSchema = { @@ -84,7 +84,7 @@ const StockObjectSchema: Yup.ObjectSchema = Yup.object({ .required('Produk wajib diisi!') .min(1, 'Produk wajib diisi!') .typeError('Produk harus berupa angka!'), - usage_qty: Yup.number() + qty: Yup.number() .required('Jumlah penggunaan wajib diisi!') .min(1, 'Jumlah penggunaan tidak boleh 0!') .typeError('Jumlah penggunaan harus berupa angka!'), @@ -266,12 +266,12 @@ export const getRecordingGrowingFormInitialValues = ( stocks: initialValues?.stocks?.map( (stock: NonNullable[0]) => ({ product_warehouse_id: stock.product_warehouse_id, - usage_qty: stock.usage_qty, + qty: stock.qty, }) ) ?? [ { product_warehouse_id: 0, - usage_qty: '', + qty: '', }, ], depletions: initialValues?.depletions?.map( diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 020de967..26d6aad7 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -380,25 +380,25 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const layingValues = values as RecordingLayingFormValues; const layingPayload = { - project_flock_kandangs_id: layingValues.project_flock_kandangs_id, + project_flock_kandang_id: layingValues.project_flock_kandangs_id, body_weights: (layingValues.body_weights ?? []).map((bw) => ({ avg_weight: typeof bw.avg_weight === 'number' ? bw.avg_weight : parseFloat(String(bw.avg_weight)) || 0, - qty: bw.qty || 0, + qty: Number(bw.qty) || 0, })), stocks: (layingValues.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, - usage_qty: stock.usage_qty || 0, + qty: Number(stock.qty) || 0, })), depletions: (layingValues.depletions ?? []).map((depletion) => ({ product_warehouse_id: depletion.product_warehouse_id, - qty: depletion.qty || 0, + qty: Number(depletion.qty) || 0, })), eggs: (layingValues.eggs ?? []).map((egg) => ({ product_warehouse_id: egg.product_warehouse_id, - qty: egg.qty || 0, + qty: Number(egg.qty) || 0, })), }; @@ -419,21 +419,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const growingValues = values as RecordingGrowingFormValues; const growingPayload = { - project_flock_kandangs_id: growingValues.project_flock_kandangs_id, + project_flock_kandang_id: growingValues.project_flock_kandangs_id, body_weights: (growingValues.body_weights ?? []).map((bw) => ({ avg_weight: typeof bw.avg_weight === 'number' ? bw.avg_weight : parseFloat(String(bw.avg_weight)) || 0, - qty: bw.qty || 0, + qty: Number(bw.qty) || 0, })), stocks: (growingValues.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, - usage_qty: stock.usage_qty || 0, + qty: Number(stock.qty) || 0, })), depletions: (growingValues.depletions ?? []).map((depletion) => ({ product_warehouse_id: depletion.product_warehouse_id, - qty: depletion.qty || 0, + qty: Number(depletion.qty) || 0, })), }; @@ -493,7 +493,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const stock = formik.values.stocks?.[stockIdx]; if (!stock || !stock.product_warehouse_id) return null; const availableStock = getAvailableStock(stock.product_warehouse_id); - const requestedUsage = Number(stock.usage_qty) || 0; + const requestedUsage = Number(stock.qty) || 0; if (requestedUsage > availableStock) { return `Jumlah pakai melebihi stok tersedia! Maksimal: ${availableStock.toLocaleString('en-US')}`; } @@ -508,7 +508,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const stock = formik.values.stocks?.[stockIdx]; if (!stock || !stock.product_warehouse_id) return null; const availableStock = getAvailableStock(stock.product_warehouse_id); - const requestedUsage = Number(stock.usage_qty) || 0; + const requestedUsage = Number(stock.qty) || 0; const remainingStock = availableStock - requestedUsage; if (requestedUsage > 0) { return ( @@ -864,7 +864,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { ...(formik.values.stocks || []), { product_warehouse_id: 0, - usage_qty: '', + qty: '', }, ]; formik.setFieldValue('stocks', newStocks); @@ -873,7 +873,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const handleStockUsageQtyChangeWrapper = useCallback( (idx: number) => (e: React.ChangeEvent) => { const value = parseFloat(e.target.value) || 0; - formik.setFieldValue(`stocks.${idx}.usage_qty`, value); + formik.setFieldValue(`stocks.${idx}.qty`, value); }, [formik] ); @@ -1578,8 +1578,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{ thousandSeparator=',' decimalSeparator='.' isError={ - isRepeaterInputError('stocks', 'usage_qty', idx) + isRepeaterInputError('stocks', 'qty', idx) .isError || Boolean(getStockUsageError(idx)) } errorMessage={ - isRepeaterInputError('stocks', 'usage_qty', idx) + isRepeaterInputError('stocks', 'qty', idx) .errorMessage || getStockUsageError(idx) || undefined diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 171ba09e..34f49c08 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -69,15 +69,14 @@ export type Recording = BaseMetadata & }; export type CreateGrowingRecordingPayload = { - project_flock_kandangs_id: number; + project_flock_kandang_id: number; body_weights: { avg_weight: number; qty: number; }[]; stocks?: { product_warehouse_id: number; - usage_qty: number; - pending_qty?: number; + qty: number; }[]; depletions?: { product_warehouse_id: number; From b976600099bd3503c03658fbbf75e6ff86f0e0f5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 2 Nov 2025 23:22:37 +0700 Subject: [PATCH 020/101] refactor(FE-170,174): update GradingForm to include recording_egg_id in grading data enhance validation schema --- .../recording/form/RecordingForm.schema.ts | 23 +++++------- .../recording/grading/form/GradingForm.tsx | 36 +++++++++++-------- src/types/api/production/recording.d.ts | 4 +-- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index f91a4e7b..6660e491 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -36,8 +36,8 @@ type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & { }; type RecordingGradingFormSchemaType = { - recording_egg_id: number; eggs_grading: { + recording_egg_id: number; grade: string; qty: number | string; }[]; @@ -193,13 +193,13 @@ export const UpdateRecordingLayingFormSchema = RecordingLayingFormSchema.shape({ export const RecordingGradingFormSchema: Yup.ObjectSchema = Yup.object({ - recording_egg_id: Yup.number() - .required('Recording Egg ID wajib diisi!') - .min(1, 'Recording Egg ID minimal 1!') - .typeError('Recording Egg ID harus berupa angka!'), eggs_grading: Yup.array() .of( Yup.object({ + recording_egg_id: Yup.number() + .required('Recording Egg ID wajib diisi!') + .min(1, 'Recording Egg ID minimal 1!') + .typeError('Recording Egg ID harus berupa angka!'), grade: Yup.string() .required('Grade telur wajib diisi!') .typeError('Grade telur harus berupa string!'), @@ -213,13 +213,7 @@ export const RecordingGradingFormSchema: Yup.ObjectSchema + initialValues?: Partial & { recording_egg_id?: number } ): RecordingGradingFormValues => ({ - recording_egg_id: initialValues?.recording_egg_id ?? 0, eggs_grading: initialValues?.eggs_grading?.map((grading) => ({ + recording_egg_id: grading.recording_egg_id, grade: grading.grade, qty: grading.qty, })) ?? [ { + recording_egg_id: initialValues?.recording_egg_id ?? 0, grade: '', qty: '', }, diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index d07e61b1..57e55f3d 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -3,7 +3,6 @@ import { useMemo, useState, useEffect, useCallback } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useFormik } from 'formik'; -import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; @@ -12,7 +11,6 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { FormHeader } from '@/components/helper/form/FormHeader'; -import { RecordingApi } from '@/services/api/production'; import { CreateGradingPayload, UpdateGradingPayload, @@ -50,8 +48,6 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { const [formSteps, setFormSteps] = useState(null); // ===== API DATA FETCHING ===== - // Fetch existing gradings for the recording egg to show recorded data - // Optional: Fetch existing gradings if needed for display purposes // const existingGradingsUrl = useMemo(() => { // const recordingEggIdToUse = recordingId || initialValues?.id?.toString(); // if (!recordingEggIdToUse) return null; @@ -64,7 +60,6 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { // ); // ===== DATA PROCESSING ===== - // No data processing needed - grading form only needs recording_egg_id and grading data // ===== FORM HANDLERS ===== const { @@ -88,6 +83,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { recording_egg_id: recordingEggId, eggs_grading: initialValues?.grading_eggs?.map((grading: GradingEgg) => ({ + recording_egg_id: recordingEggId, grade: grading.grade, qty: grading.qty, })) || [], @@ -105,8 +101,8 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { validateOnBlur: true, onSubmit: async (values) => { const gradingPayload = { - recording_egg_id: values.recording_egg_id, eggs_grading: (values.eggs_grading ?? []).map((grading) => ({ + recording_egg_id: grading.recording_egg_id, grade: grading.grade, qty: grading.qty || 0, })), @@ -126,15 +122,20 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { }, }); - // Grading form doesn't need approval/reject handlers - // Grading Handlers const addGrading = () => { + let recordingEggId: number | undefined = initialValues?.id; + + if (!recordingEggId) { + recordingEggId = parseInt(recordingId || '0') || 0; + } + const newGrading = [ ...(formik.values.eggs_grading || []), { + recording_egg_id: recordingEggId, grade: '', - qty: 0, + qty: '', }, ]; formik.setFieldValue('eggs_grading', newGrading); @@ -223,12 +224,18 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { useEffect(() => { if (formik.values.eggs_grading && formik.values.eggs_grading.length === 0) { - formik.setFieldValue('eggs_grading', [{ grade: '', qty: 0 }]); + let recordingEggId: number | undefined = initialValues?.id; + + if (!recordingEggId) { + recordingEggId = parseInt(recordingId || '0') || 0; + } + + formik.setFieldValue('eggs_grading', [ + { recording_egg_id: recordingEggId, grade: '', qty: '' }, + ]); } }, [formik]); - // Grading form doesn't need loading state since it only depends on recording_egg_id - return ( <>
@@ -320,7 +327,8 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { ) : ( <> - Recording Egg ID#{formik.values.recording_egg_id} + Recording Egg ID# + {formik.values.eggs_grading?.[0]?.recording_egg_id || '-'} )}
@@ -473,7 +481,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { className={{ wrapper: 'w-full min-w-24', }} - placeholder='Jumlah' + placeholder='Masukkan jumlah telur' /> {type !== 'detail' && ( diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 34f49c08..5b544175 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -85,8 +85,8 @@ export type CreateGrowingRecordingPayload = { }; export type CreateGradingPayload = { - recording_egg_id: number; eggs_grading: { + recording_egg_id: number; grade: string; qty: number; }[]; @@ -95,8 +95,8 @@ export type CreateGradingPayload = { export type UpdateGradingPayload = CreateGradingPayload; export type CreateGradingRecordingPayload = { - recording_egg_id: number; eggs_grading: { + recording_egg_id: number; grade: string; qty: number; }[]; From c26e17488598f4948dfea9c3c075fe4516a7a1d8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 3 Nov 2025 09:00:49 +0700 Subject: [PATCH 021/101] refactor(FE-174): enhance RecordingApi by adding approve and reject methods for better approval handling --- .../production/recording/RecordingTable.tsx | 28 ++++--------- .../recording/form/RecordingForm.tsx | 28 ++++--------- src/services/api/production.ts | 41 ++++++++++++++++++- 3 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 5e018e61..45ed54a5 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -226,16 +226,10 @@ const RecordingTable = () => { const bulkApproveHandler = async () => { setIsBulkApproveLoading(true); - const approveResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'APPROVED', - approvable_ids: selectedRowIds, - notes: 'Bulk Approved', - }, - }); + const approveResponse = await RecordingApi.approve( + selectedRowIds, + 'Bulk Approved' + ); if (isResponseSuccess(approveResponse)) { await refreshRecordings(); @@ -255,16 +249,10 @@ const RecordingTable = () => { const bulkRejectHandler = async () => { setIsBulkRejectLoading(true); - const rejectResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'REJECTED', - approvable_ids: selectedRowIds, - notes: 'Bulk Rejected', - }, - }); + const rejectResponse = await RecordingApi.reject( + selectedRowIds, + 'Bulk Rejected' + ); if (isResponseSuccess(rejectResponse)) { refreshRecordings(); diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 26d6aad7..7e888cd5 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -696,16 +696,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const approveHandler = async () => { setIsApproveLoading(true); - const approveResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'APPROVED', - approvable_ids: [initialValues?.id as number], - notes: 'Approved via Form', - }, - }); + const approveResponse = await RecordingApi.approve( + initialValues?.id as number, + 'Approved via Form' + ); if (isResponseSuccess(approveResponse)) { toast.success('Recording berhasil disetujui!'); @@ -724,16 +718,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const rejectHandler = async () => { setIsRejectLoading(true); - const rejectResponse = await RecordingApi.customRequest< - BaseApiResponse - >('approvals', { - method: 'POST', - payload: { - action: 'REJECTED', - approvable_ids: [initialValues?.id as number], - notes: 'Rejected via Form', - }, - }); + const rejectResponse = await RecordingApi.reject( + initialValues?.id as number, + 'Rejected via Form' + ); if (isResponseSuccess(rejectResponse)) { toast.success('Recording berhasil ditolak!'); diff --git a/src/services/api/production.ts b/src/services/api/production.ts index 5c2754c8..788b9eb7 100644 --- a/src/services/api/production.ts +++ b/src/services/api/production.ts @@ -1,4 +1,5 @@ import { BaseApiService } from './base'; +import { BaseApiResponse } from '@/types/api/api-general'; import { CreateProjectFlockPayload, ProjectFlock, @@ -20,11 +21,47 @@ export const ProjectFlockApi = new BaseApiService< CreateProjectFlockPayload, UpdateProjectFlockPayload >('/production/project_flocks'); -export const RecordingApi = new BaseApiService< +export class RecordingService extends BaseApiService< Recording, CreateRecordingPayload, UpdateRecordingPayload ->('/production/recordings'); +> { + constructor(basePath: string = '') { + super(basePath); + } + + async approve( + idOrIds: number | number[], + notes: string = 'Approved via Form' + ): Promise | undefined> { + const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds]; + return await this.customRequest>('approvals', { + method: 'POST', + payload: { + action: 'APPROVED', + approvable_ids, + notes, + }, + }); + } + + async reject( + idOrIds: number | number[], + notes: string = 'Rejected via Form' + ): Promise | undefined> { + const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds]; + return await this.customRequest>('approvals', { + method: 'POST', + payload: { + action: 'REJECTED', + approvable_ids, + notes, + }, + }); + } +} + +export const RecordingApi = new RecordingService('/production/recordings'); export const ChickinApi = new BaseApiService< Chickin, CreateChickinPayload, From 9a4d961deef4e58670e1fbb1b7b9dd48149a52ca Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 3 Nov 2025 09:28:20 +0700 Subject: [PATCH 022/101] refactor(FE-174): implement create, update, and delete grading methods in RecordingApi and update handlers --- .../recording/grading/form/GradingForm.tsx | 1 - .../grading/form/useGradingFormHandlers.ts | 45 +++++++++++-------- src/services/api/production.ts | 35 +++++++++++++++ 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 57e55f3d..4f41fc16 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -29,7 +29,6 @@ import toast from 'react-hot-toast'; import Card from '@/components/Card'; import StepItem from '@/components/steps/StepItem'; -import { useModal } from '@/components/Modal'; import { useGradingFormHandlers } from './useGradingFormHandlers'; interface GradingFormProps { diff --git a/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts b/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts index c24a644b..6cc5c8b0 100644 --- a/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts +++ b/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts @@ -19,14 +19,15 @@ export const useGradingFormHandlers = (gradingId?: number) => { const createGradingHandler = useCallback( async (payload: CreateGradingPayload) => { - const res = await RecordingApi.customRequest('gradings', { - method: 'POST', - payload, - }) as BaseApiResponse; - if (isResponseError(res)) { - setRecordingFormErrorMessage(res.message); + const res = (await RecordingApi.createGrading(payload)) as + | BaseApiResponse + | undefined; + + if (!res || isResponseError(res)) { + setRecordingFormErrorMessage(res?.message || 'Failed to add Grading'); return; } + toast.success(res?.message || 'Successfully added Grading!'); router.push('/production/recording'); }, @@ -35,12 +36,14 @@ export const useGradingFormHandlers = (gradingId?: number) => { const updateGradingHandler = useCallback( async (gradingId: number, payload: UpdateGradingPayload) => { - const res = await RecordingApi.customRequest(`gradings/${gradingId}`, { - method: 'PUT', - payload, - }) as BaseApiResponse; - if (isResponseError(res)) { - setRecordingFormErrorMessage(res.message); + const res = (await RecordingApi.updateGrading(gradingId, payload)) as + | BaseApiResponse + | undefined; + + if (!res || isResponseError(res)) { + setRecordingFormErrorMessage( + res?.message || 'Failed to update Grading' + ); return; } toast.success(res?.message || 'Successfully updated Grading!'); @@ -59,17 +62,21 @@ export const useGradingFormHandlers = (gradingId?: number) => { setIsDeleteLoading(true); try { - const res = await RecordingApi.customRequest(`gradings/${gradingId}`, { - method: 'DELETE', - }) as BaseApiResponse; - if (isResponseError(res)) { - setRecordingFormErrorMessage(res.message); + const res = (await RecordingApi.deleteGrading(gradingId)) as + | BaseApiResponse + | undefined; + + if (!res || isResponseError(res)) { + setRecordingFormErrorMessage( + res?.message || 'Failed to delete Grading' + ); return; } deleteModal.closeModal(); toast.success(res?.message || 'Successfully delete Grading!'); router.push('/production/recording'); - } catch (error) { + } catch (err) { + console.error(err); setRecordingFormErrorMessage('Failed to delete Grading'); } finally { setIsDeleteLoading(false); @@ -85,4 +92,4 @@ export const useGradingFormHandlers = (gradingId?: number) => { deleteRecordingClickHandler, confirmationModalDeleteClickHandler, }; -}; \ No newline at end of file +}; diff --git a/src/services/api/production.ts b/src/services/api/production.ts index 788b9eb7..a2853803 100644 --- a/src/services/api/production.ts +++ b/src/services/api/production.ts @@ -9,6 +9,8 @@ import { CreateRecordingPayload, Recording, UpdateRecordingPayload, + CreateGradingPayload, + UpdateGradingPayload, } from '@/types/api/production/recording'; import { Chickin, @@ -59,6 +61,39 @@ export class RecordingService extends BaseApiService< }, }); } + + async createGrading( + payload: CreateGradingPayload + ): Promise | undefined> { + return await this.customRequest>('gradings', { + method: 'POST', + payload, + }); + } + + async updateGrading( + gradingId: number, + payload: UpdateGradingPayload + ): Promise | undefined> { + return await this.customRequest>( + `gradings/${gradingId}`, + { + method: 'PUT', + payload, + } + ); + } + + async deleteGrading( + gradingId: number + ): Promise | undefined> { + return await this.customRequest>( + `gradings/${gradingId}`, + { + method: 'DELETE', + } + ); + } } export const RecordingApi = new RecordingService('/production/recordings'); From ac11559754870720321a9c9e186a7ed2df10c9f6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 3 Nov 2025 09:35:52 +0700 Subject: [PATCH 023/101] chore(FE-Storyless): remove inputmask package and its type definitions from dependencies --- package-lock.json | 27 +++++++++++---------------- package.json | 2 -- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33b7c640..2fa0e38b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", - "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -31,7 +30,6 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", - "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -1639,13 +1637,6 @@ "@types/react": "*" } }, - "node_modules/@types/inputmask": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/inputmask/-/inputmask-5.0.7.tgz", - "integrity": "sha512-uojbVPWzBQ/n/0jc/d16fLqmGasFIptbrLD2WrCPWArlk+5PgblOqH4EDkI3AoobXLAlOK5yF01V8jMmvMG5qg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1681,6 +1672,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1750,6 +1742,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2267,6 +2260,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2800,7 +2794,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.3.10", @@ -3228,6 +3223,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3401,6 +3397,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4203,12 +4200,6 @@ "node": ">=0.8.19" } }, - "node_modules/inputmask": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/inputmask/-/inputmask-5.0.9.tgz", - "integrity": "sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==", - "license": "MIT" - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5745,6 +5736,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5754,6 +5746,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6552,6 +6545,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6719,6 +6713,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 10fe9598..1d181a40 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "axios": "^1.12.2", "clsx": "^2.1.1", "formik": "^2.4.6", - "inputmask": "^5.0.9", "moment": "^2.30.1", "next": "15.5.3", "react": "19.1.0", @@ -34,7 +33,6 @@ "@eslint/eslintrc": "^3", "@iconify/react": "^6.0.2", "@tailwindcss/postcss": "^4", - "@types/inputmask": "^5.0.7", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", From 2ba23654ceffa3de871aca6d4deb2007f6b2ff4e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 3 Nov 2025 09:44:50 +0700 Subject: [PATCH 024/101] refactor(FE-Storyless): simplify RowOptionsMenu by using RowOptionsMenuWrapper and enhance button layout --- .../inventory/movement/MovementTable.tsx | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/src/components/pages/inventory/movement/MovementTable.tsx b/src/components/pages/inventory/movement/MovementTable.tsx index 6ed4c49a..8ff39e3d 100644 --- a/src/components/pages/inventory/movement/MovementTable.tsx +++ b/src/components/pages/inventory/movement/MovementTable.tsx @@ -20,6 +20,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import SelectInput from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; +import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; const RowOptionsMenu = ({ type = 'dropdown', @@ -27,30 +28,19 @@ const RowOptionsMenu = ({ }: { type: 'dropdown' | 'collapse'; props: CellContext; -}) => { - return ( -
( + + -
- ); -}; + + Detail + + +); const MovementTable = () => { const { @@ -175,7 +165,7 @@ const MovementTable = () => { {currentPageSize <= 2 && ( - + )} @@ -188,15 +178,16 @@ const MovementTable = () => { <>
-
-
+
+
From e9e8ad771eafac1db87fe02b0c175b283282ed65 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 3 Nov 2025 10:22:35 +0700 Subject: [PATCH 025/101] refactor(FE-174): enhance GradingForm and RecordingForm with improved error handling and modal integration for delete actions --- .../recording/form/RecordingForm.tsx | 66 ++++++++++--- .../form/useRecordingFormHandlers.ts | 70 -------------- .../recording/grading/form/GradingForm.tsx | 86 ++++++++++++++--- .../grading/form/useGradingFormHandlers.ts | 95 ------------------- 4 files changed, 126 insertions(+), 191 deletions(-) delete mode 100644 src/components/pages/production/recording/form/useRecordingFormHandlers.ts delete mode 100644 src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 7e888cd5..e6eba21c 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -31,11 +31,10 @@ import { UpdateRecordingGrowingFormSchema, UpdateRecordingLayingFormSchema, } from './RecordingForm.schema'; -import { useRecordingFormHandlers } from './useRecordingFormHandlers'; import { ProjectFlockApi } from '@/services/api/production'; import { LocationApi } from '@/services/api/master-data'; import { ProductWarehouseApi } from '@/services/api/inventory'; -import { isResponseSuccess } from '@/lib/api-helper'; +import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; import { useModal } from '@/components/Modal'; @@ -77,9 +76,61 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); const [formSteps, setFormSteps] = useState(null); + const [recordingFormErrorMessage, setRecordingFormErrorMessage] = + useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); const approveModal = useModal(); const rejectModal = useModal(); + const deleteModal = useModal(); + + // ===== FORM HANDLERS ===== + const createRecordingHandler = useCallback( + async ( + payload: CreateGrowingRecordingPayload | CreateLayingRecordingPayload + ) => { + const res = await RecordingApi.create(payload); + if (isResponseError(res)) { + setRecordingFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + router.push('/production/recording'); + }, + [router] + ); + + const updateRecordingHandler = useCallback( + async ( + recordingId: number, + payload: UpdateGrowingRecordingPayload | UpdateLayingRecordingPayload + ) => { + const res = await RecordingApi.update(recordingId, payload); + if (res?.status === 'error') { + setRecordingFormErrorMessage(res.message); + return; + } + toast.success(res?.message as string); + router.refresh(); + router.push('/production/recording'); + }, + [router] + ); + + const deleteRecordingClickHandler = useCallback(() => { + deleteModal.openModal(); + }, [deleteModal]); + + const confirmationModalDeleteClickHandler = useCallback(async () => { + if (!initialValues?.id) return; + + setIsDeleteLoading(true); + await RecordingApi.delete(initialValues.id); + deleteModal.closeModal(); + toast.success('Successfully delete Recording!'); + setIsDeleteLoading(false); + router.push('/production/recording'); + }, [deleteModal, initialValues?.id, router]); // ===== API DATA FETCHING ===== const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ @@ -336,17 +387,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return options; }, [eggProductsData]); - // ===== FORM HANDLERS ===== - const { - deleteModal, - recordingFormErrorMessage, - isDeleteLoading, - createRecordingHandler, - updateRecordingHandler, - deleteRecordingClickHandler, - confirmationModalDeleteClickHandler, - } = useRecordingFormHandlers(initialValues?.id); - const isLayingCategory = projectFlockKandangLookup?.project_flock?.category === 'LAYING'; diff --git a/src/components/pages/production/recording/form/useRecordingFormHandlers.ts b/src/components/pages/production/recording/form/useRecordingFormHandlers.ts deleted file mode 100644 index 58893ce1..00000000 --- a/src/components/pages/production/recording/form/useRecordingFormHandlers.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useCallback, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { toast } from 'react-hot-toast'; -import { useModal } from '@/components/Modal'; -import { RecordingApi } from '@/services/api/production'; -import { - CreateRecordingPayload, - UpdateRecordingPayload, -} from '@/types/api/production/recording'; -import { isResponseError } from '@/lib/api-helper'; - -export const useRecordingFormHandlers = (initialValuesId?: number) => { - const router = useRouter(); - const deleteModal = useModal(); - const [recordingFormErrorMessage, setRecordingFormErrorMessage] = - useState(''); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const createRecordingHandler = useCallback( - async (payload: CreateRecordingPayload) => { - const res = await RecordingApi.create(payload); - if (isResponseError(res)) { - setRecordingFormErrorMessage(res.message); - return; - } - toast.success(res?.message as string); - router.push('/production/recording'); - }, - [router] - ); - - const updateRecordingHandler = useCallback( - async (recordingId: number, payload: UpdateRecordingPayload) => { - const res = await RecordingApi.update(recordingId, payload); - if (res?.status === 'error') { - setRecordingFormErrorMessage(res.message); - return; - } - toast.success(res?.message as string); - router.refresh(); - router.push('/production/recording'); - }, - [router] - ); - - const deleteRecordingClickHandler = useCallback(() => { - deleteModal.openModal(); - }, [deleteModal]); - - const confirmationModalDeleteClickHandler = useCallback(async () => { - if (!initialValuesId) return; - - setIsDeleteLoading(true); - await RecordingApi.delete(initialValuesId); - deleteModal.closeModal(); - toast.success('Successfully delete Recording!'); - setIsDeleteLoading(false); - router.push('/production/recording'); - }, [deleteModal, initialValuesId, router]); - - return { - deleteModal, - recordingFormErrorMessage, - isDeleteLoading, - createRecordingHandler, - updateRecordingHandler, - deleteRecordingClickHandler, - confirmationModalDeleteClickHandler, - }; -}; diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 4f41fc16..9979c2cc 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -17,7 +17,10 @@ import { RecordingEgg, GradingEgg, } from '@/types/api/production/recording'; -import { type FormStepStatus } from '@/types/api/api-general'; +import { + type FormStepStatus, + type BaseApiResponse, +} from '@/types/api/api-general'; import { RecordingGradingFormSchema, RecordingGradingFormValues, @@ -26,10 +29,12 @@ import { } from '../../form/RecordingForm.schema'; import { cn } from '@/lib/helper'; import toast from 'react-hot-toast'; +import { RecordingApi } from '@/services/api/production'; +import { isResponseError } from '@/lib/api-helper'; +import { useModal } from '@/components/Modal'; import Card from '@/components/Card'; import StepItem from '@/components/steps/StepItem'; -import { useGradingFormHandlers } from './useGradingFormHandlers'; interface GradingFormProps { type?: 'add' | 'edit' | 'detail'; @@ -45,6 +50,9 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { ); const [formSteps, setFormSteps] = useState(null); + const [gradingFormErrorMessage, setGradingFormErrorMessage] = useState(''); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const deleteModal = useModal(); // ===== API DATA FETCHING ===== // const existingGradingsUrl = useMemo(() => { @@ -61,15 +69,67 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { // ===== DATA PROCESSING ===== // ===== FORM HANDLERS ===== - const { - deleteModal, - recordingFormErrorMessage, - isDeleteLoading, - createGradingHandler, - updateGradingHandler, - deleteRecordingClickHandler, - confirmationModalDeleteClickHandler, - } = useGradingFormHandlers(initialValues?.id); + const createGradingHandler = useCallback( + async (payload: CreateGradingPayload) => { + const res = (await RecordingApi.createGrading(payload)) as + | BaseApiResponse + | undefined; + + if (!res || isResponseError(res)) { + setGradingFormErrorMessage(res?.message || 'Failed to add Grading'); + return; + } + + toast.success(res?.message || 'Successfully added Grading!'); + router.push('/production/recording'); + }, + [router] + ); + + const updateGradingHandler = useCallback( + async (gradingId: number, payload: UpdateGradingPayload) => { + const res = (await RecordingApi.updateGrading(gradingId, payload)) as + | BaseApiResponse + | undefined; + + if (!res || isResponseError(res)) { + setGradingFormErrorMessage(res?.message || 'Failed to update Grading'); + return; + } + toast.success(res?.message || 'Successfully updated Grading!'); + router.refresh(); + router.push('/production/recording'); + }, + [router] + ); + + const deleteRecordingClickHandler = useCallback(() => { + deleteModal.openModal(); + }, [deleteModal]); + + const confirmationModalDeleteClickHandler = useCallback(async () => { + if (!initialValues?.id) return; + + setIsDeleteLoading(true); + try { + const res = (await RecordingApi.deleteGrading(initialValues.id)) as + | BaseApiResponse + | undefined; + + if (!res || isResponseError(res)) { + setGradingFormErrorMessage(res?.message || 'Failed to delete Grading'); + return; + } + deleteModal.closeModal(); + toast.success(res?.message || 'Successfully delete Grading!'); + router.push('/production/recording'); + } catch (err) { + console.error(err); + setGradingFormErrorMessage('Failed to delete Grading'); + } finally { + setIsDeleteLoading(false); + } + }, [deleteModal, initialValues?.id, router]); const formikInitialValues = useMemo(() => { let recordingEggId: number | undefined = initialValues?.id; @@ -599,14 +659,14 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => {
)}
- {recordingFormErrorMessage && ( + {gradingFormErrorMessage && (
- {recordingFormErrorMessage} + {gradingFormErrorMessage}
)} diff --git a/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts b/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts deleted file mode 100644 index 6cc5c8b0..00000000 --- a/src/components/pages/production/recording/grading/form/useGradingFormHandlers.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { useCallback, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { toast } from 'react-hot-toast'; -import { useModal } from '@/components/Modal'; -import { RecordingApi } from '@/services/api/production'; -import { - CreateGradingPayload, - UpdateGradingPayload, -} from '@/types/api/production/recording'; -import { isResponseError } from '@/lib/api-helper'; -import { type BaseApiResponse } from '@/types/api/api-general'; - -export const useGradingFormHandlers = (gradingId?: number) => { - const router = useRouter(); - const deleteModal = useModal(); - const [recordingFormErrorMessage, setRecordingFormErrorMessage] = - useState(''); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - const createGradingHandler = useCallback( - async (payload: CreateGradingPayload) => { - const res = (await RecordingApi.createGrading(payload)) as - | BaseApiResponse - | undefined; - - if (!res || isResponseError(res)) { - setRecordingFormErrorMessage(res?.message || 'Failed to add Grading'); - return; - } - - toast.success(res?.message || 'Successfully added Grading!'); - router.push('/production/recording'); - }, - [router] - ); - - const updateGradingHandler = useCallback( - async (gradingId: number, payload: UpdateGradingPayload) => { - const res = (await RecordingApi.updateGrading(gradingId, payload)) as - | BaseApiResponse - | undefined; - - if (!res || isResponseError(res)) { - setRecordingFormErrorMessage( - res?.message || 'Failed to update Grading' - ); - return; - } - toast.success(res?.message || 'Successfully updated Grading!'); - router.refresh(); - router.push('/production/recording'); - }, - [router] - ); - - const deleteRecordingClickHandler = useCallback(() => { - deleteModal.openModal(); - }, [deleteModal]); - - const confirmationModalDeleteClickHandler = useCallback(async () => { - if (!gradingId) return; - - setIsDeleteLoading(true); - try { - const res = (await RecordingApi.deleteGrading(gradingId)) as - | BaseApiResponse - | undefined; - - if (!res || isResponseError(res)) { - setRecordingFormErrorMessage( - res?.message || 'Failed to delete Grading' - ); - return; - } - deleteModal.closeModal(); - toast.success(res?.message || 'Successfully delete Grading!'); - router.push('/production/recording'); - } catch (err) { - console.error(err); - setRecordingFormErrorMessage('Failed to delete Grading'); - } finally { - setIsDeleteLoading(false); - } - }, [deleteModal, gradingId, router]); - - return { - deleteModal, - recordingFormErrorMessage, - isDeleteLoading, - createGradingHandler, - updateGradingHandler, - deleteRecordingClickHandler, - confirmationModalDeleteClickHandler, - }; -}; From bcb4d4492d01ebce05f008544e309fdedc7d2d51 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 3 Nov 2025 10:30:33 +0700 Subject: [PATCH 026/101] refactor(FE-170): replace FormHeader with custom header in GradingForm and RecordingForm for improved layout and navigation --- .../recording/form/RecordingForm.tsx | 22 +++++++++++++------ .../recording/grading/form/GradingForm.tsx | 22 +++++++++++++------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index e6eba21c..30aee0ac 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -11,7 +11,6 @@ import NumberInput from '@/components/input/NumberInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import { FormHeader } from '@/components/helper/form/FormHeader'; import { RecordingApi } from '@/services/api/production'; import { CreateGrowingRecordingPayload, @@ -1058,12 +1057,21 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return ( <>
- - +
+ +

+ {type === 'add' && 'Tambah Recording'} + {type === 'edit' && 'Edit Recording'} + {type === 'detail' && 'Detail Recording'} +

+
{/* Project Flock Info Card */} {projectFlockKandangLookup && (
diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 9979c2cc..0acaaa02 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -10,7 +10,6 @@ import NumberInput from '@/components/input/NumberInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import { FormHeader } from '@/components/helper/form/FormHeader'; import { CreateGradingPayload, UpdateGradingPayload, @@ -298,12 +297,21 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { return ( <>
- - +
+ +

+ {type === 'add' && 'Tambah Grading'} + {type === 'edit' && 'Edit Grading'} + {type === 'detail' && 'Detail Grading'} +

+
{/* Project Flock Info Card */}
{/* Form Steps */} From d8599a850aa9613e6eb9b441b84afdfab1019b4b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 3 Nov 2025 10:40:13 +0700 Subject: [PATCH 027/101] refactor(FE-170): streamline RecordingTable component by removing unused state and optimizing layout for better usability --- .../production/recording/RecordingTable.tsx | 522 ++++-------------- 1 file changed, 114 insertions(+), 408 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index b4dbee3a..722589c8 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -1,26 +1,22 @@ 'use client'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useState } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; -import { SortingState } from '@tanstack/react-table'; +import { SortingState, CellContext } from '@tanstack/react-table'; import { cn } from '@/lib/helper'; import { useModal } from '@/components/Modal'; import Button from '@/components/Button'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import { OptionType } from '@/components/input/SelectInput'; import SelectInput from '@/components/input/SelectInput'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import { ROWS_OPTIONS } from '@/config/constant'; -import CheckboxInput from '@/components/input/CheckboxInput'; -import { TableToolbar } from '@/components/table/TableToolbar'; -import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector'; import Table from '@/components/Table'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; -import { type CellContext } from '@tanstack/react-table'; import { type Recording } from '@/types/api/production/recording'; -import { type BaseApiResponse } from '@/types/api/api-general'; import { RecordingApi } from '@/services/api/production'; import { AreaApi } from '@/services/api/master-data'; import { LocationApi } from '@/services/api/master-data'; @@ -103,17 +99,12 @@ const RecordingTable = () => { }); const [sorting, setSorting] = useState([]); - const [rowSelection, setRowSelection] = useState>({}); const [selectedRecording, setSelectedRecording] = useState< Recording | undefined >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [isBulkApproveLoading, setIsBulkApproveLoading] = useState(false); - const [isBulkRejectLoading, setIsBulkRejectLoading] = useState(false); const singleDeleteModal = useModal(); - const bulkApproveModal = useModal(); - const bulkRejectModal = useModal(); // State for dropdown search const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); @@ -205,62 +196,6 @@ const RecordingTable = () => { [setPageSize, setPage] ); - const paginatedData = useMemo(() => { - if (!recordings || recordings.status !== 'success') return []; - - return recordings.data; - }, [recordings]); - - const selectedRowIds = Object.keys(rowSelection).map((item) => - parseInt(item) - ); - - const bulkApproveHandler = async () => { - setIsBulkApproveLoading(true); - - const approveResponse = await RecordingApi.approve( - selectedRowIds, - 'Bulk Approved' - ); - - if (isResponseSuccess(approveResponse)) { - await refreshRecordings(); - setRowSelection({}); - bulkApproveModal.closeModal(); - toast.success( - `Successfully approved ${selectedRowIds.length} recordings!` - ); - } - if (isResponseError(approveResponse)) { - toast.error(approveResponse?.message as string); - bulkApproveModal.closeModal(); - } - setIsBulkApproveLoading(false); - }; - - const bulkRejectHandler = async () => { - setIsBulkRejectLoading(true); - - const rejectResponse = await RecordingApi.reject( - selectedRowIds, - 'Bulk Rejected' - ); - - if (isResponseSuccess(rejectResponse)) { - refreshRecordings(); - setRowSelection({}); - bulkRejectModal.closeModal(); - toast.success( - `Successfully rejected ${selectedRowIds.length} recordings!` - ); - } - if (isResponseError(rejectResponse)) { - toast.error(rejectResponse?.message as string); - bulkRejectModal.closeModal(); - } - setIsBulkRejectLoading(false); - }; - const singleDeleteHandler = async () => { setIsDeleteLoading(true); @@ -273,315 +208,123 @@ const RecordingTable = () => { }; return ( -
+
- - - - {/* Filter Dropdowns - Desktop */} -
- { - const selectedValue = selected as OptionType | null; - setSelectedArea(selectedValue); - setSelectedLocation(null); - setSelectedKandang(null); - updateFilter( - 'areaFilter', - selectedValue ? selectedValue.value.toString() : '' - ); - updateFilter('locationFilter', ''); - updateFilter('kandangFilter', ''); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - onInputChange={(value) => setAreaSelectInputValue(value)} - isLoading={isLoadingAreas} - isClearable - /> - - { - const selectedValue = selected as OptionType | null; - setSelectedLocation(selectedValue); - setSelectedKandang(null); - updateFilter( - 'locationFilter', - selectedValue ? selectedValue.value.toString() : '' - ); - updateFilter('kandangFilter', ''); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - onInputChange={(value) => setLocationSelectInputValue(value)} - isLoading={isLoadingLocations} - isClearable - isDisabled={!selectedArea} - /> - - { - const selectedValue = selected as OptionType | null; - setSelectedKandang(selectedValue); - updateFilter( - 'kandangFilter', - selectedValue ? selectedValue.value.toString() : '' - ); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - onInputChange={(value) => setKandangSelectInputValue(value)} - isLoading={isLoadingKandang} - isClearable - isDisabled={!selectedLocation} - /> - - { - const selectedValue = selected as OptionType | null; - updateFilter( - 'periodFilter', - selectedValue ? selectedValue.value.toString() : '' - ); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - isClearable - /> -
- - {/* Filter Dropdowns - Mobile */} -
- { - const selectedValue = selected as OptionType | null; - setSelectedArea(selectedValue); - setSelectedLocation(null); - setSelectedKandang(null); - updateFilter( - 'areaFilter', - selectedValue ? selectedValue.value.toString() : '' - ); - updateFilter('locationFilter', ''); - updateFilter('kandangFilter', ''); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - onInputChange={(value) => setAreaSelectInputValue(value)} - isLoading={isLoadingAreas} - isClearable - /> - - { - const selectedValue = selected as OptionType | null; - setSelectedLocation(selectedValue); - setSelectedKandang(null); - updateFilter( - 'locationFilter', - selectedValue ? selectedValue.value.toString() : '' - ); - updateFilter('kandangFilter', ''); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - onInputChange={(value) => setLocationSelectInputValue(value)} - isLoading={isLoadingLocations} - isClearable - isDisabled={!selectedArea} - /> - - { - const selectedValue = selected as OptionType | null; - setSelectedKandang(selectedValue); - updateFilter( - 'kandangFilter', - selectedValue ? selectedValue.value.toString() : '' - ); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - onInputChange={(value) => setKandangSelectInputValue(value)} - isLoading={isLoadingKandang} - isClearable - isDisabled={!selectedLocation} - /> - - { - const selectedValue = selected as OptionType | null; - updateFilter( - 'periodFilter', - selectedValue ? selectedValue.value.toString() : '' - ); - setPage(1); - }} - className={{ wrapper: 'w-full' }} - isClearable - /> -
-
- - {/* Bulk action buttons */} -
- {selectedRowIds.length > 0 && ( -
+
+
-
- )} - + +
- +
+ { + const selectedValue = selected as OptionType | null; + setSelectedArea(selectedValue); + setSelectedLocation(null); + setSelectedKandang(null); + updateFilter( + 'areaFilter', + selectedValue ? selectedValue.value.toString() : '' + ); + updateFilter('locationFilter', ''); + updateFilter('kandangFilter', ''); + setPage(1); + }} + onInputChange={setAreaSelectInputValue} + isClearable + className={{ + wrapper: 'col-span-12 sm:col-span-3', + }} + /> + + { + const selectedValue = selected as OptionType | null; + setSelectedLocation(selectedValue); + setSelectedKandang(null); + updateFilter( + 'locationFilter', + selectedValue ? selectedValue.value.toString() : '' + ); + updateFilter('kandangFilter', ''); + setPage(1); + }} + onInputChange={setLocationSelectInputValue} + isClearable + isDisabled={!selectedArea} + className={{ + wrapper: 'col-span-12 sm:col-span-3', + }} + /> + + { + const selectedValue = selected as OptionType | null; + setSelectedKandang(selectedValue); + updateFilter( + 'kandangFilter', + selectedValue ? selectedValue.value.toString() : '' + ); + setPage(1); + }} + onInputChange={setKandangSelectInputValue} + isClearable + isDisabled={!selectedLocation} + className={{ + wrapper: 'col-span-12 sm:col-span-2', + }} + /> + + +
- + data={isResponseSuccess(recordings) ? recordings?.data : []} columns={[ - { - id: 'select', - header: ({ table }) => ( -
- -
- ), - cell: ({ row }) => ( -
- -
- ), - }, { header: '#', cell: (props) => @@ -609,38 +352,6 @@ const RecordingTable = () => { cell: (props) => props.row.original.total_chick_qty?.toLocaleString() || '-', }, - { - header: 'BW', - cell: (props) => - props.row.original.avg_daily_gain?.toFixed(2) || '-', - }, - { - header: 'Pakan', - cell: (props) => - props.row.original.cum_intake?.toLocaleString() || '-', - }, - { - header: 'FCR', - cell: (props) => props.row.original.fcr_value?.toFixed(2) || '-', - }, - { - accessorKey: 'total_depletion', - header: 'Total Deplesi', - cell: (props) => props.row.original.total_depletion_qty, - }, - { - header: 'Deplesi (%)', - cell: (props) => - props.row.original.daily_depletion_rate?.toFixed(2) || '-', - }, - { - header: 'Populasi Akhir', - cell: (props) => - ( - props.row.original.total_chick_qty - - props.row.original.total_depletion_qty - )?.toLocaleString() || '-', - }, { header: 'Tanggal Submit', cell: (props) => @@ -690,23 +401,18 @@ const RecordingTable = () => { }, ]} pageSize={tableFilterState.pageSize} - page={ - recordings?.status === 'success' - ? recordings.meta?.page - : tableFilterState.page - } + page={isResponseSuccess(recordings) ? recordings?.meta?.page : 0} totalItems={ - recordings?.status === 'success' ? recordings.meta?.total_results : 0 + isResponseSuccess(recordings) ? recordings?.meta?.total_results : 0 } onPageChange={setPage} isLoading={isLoading} sorting={sorting} setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} className={{ containerClassName: cn({ - 'mb-20': paginatedData.length === 0, + 'mb-20': + isResponseSuccess(recordings) && recordings?.data?.length === 0, }), tableWrapperClassName: 'overflow-x-auto min-h-full!', tableClassName: 'font-inter w-full table-auto min-h-full!', From e4ab86c3ebfb90c630b050773c1dcf2eef1fef50 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 3 Nov 2025 10:44:37 +0700 Subject: [PATCH 028/101] refactor(FE-170): streamline RecordingForm component by native card and optimizing layout for better usability --- .../inventory/movement/form/MovementForm.tsx | 1566 ++++++++--------- 1 file changed, 776 insertions(+), 790 deletions(-) diff --git a/src/components/pages/inventory/movement/form/MovementForm.tsx b/src/components/pages/inventory/movement/form/MovementForm.tsx index 28148706..438c09c6 100644 --- a/src/components/pages/inventory/movement/form/MovementForm.tsx +++ b/src/components/pages/inventory/movement/form/MovementForm.tsx @@ -32,6 +32,7 @@ import { MovementApi } from '@/services/api/inventory'; import FileInput from '@/components/input/FileInput'; import CheckboxInput from '@/components/input/CheckboxInput'; import Badge from '@/components/Badge'; +import Card from '@/components/Card'; interface MovementFormProps { type?: 'add' | 'edit' | 'detail'; @@ -768,216 +769,270 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { className='w-full mt-8 flex flex-col gap-6' > {/* Top card - Movement details */} -
-
-
- - -
+ +
+ +
-
+ {/* Warehouse cards */}
-
-
-

Gudang Asal

- { - formik.setFieldTouched('source_warehouse', true); - formik.setFieldValue('source_warehouse', val); - formik.setFieldTouched('source_warehouse_id', true); - formik.setFieldValue( - 'source_warehouse_id', - (val as WarehouseOptionType)?.value - ); + + { + formik.setFieldTouched('source_warehouse', true); + formik.setFieldValue('source_warehouse', val); + formik.setFieldTouched('source_warehouse_id', true); + formik.setFieldValue( + 'source_warehouse_id', + (val as WarehouseOptionType)?.value + ); + }} + options={warehouseOptions} + onInputChange={setWarehouseSelectInputValue} + isLoading={isLoadingWarehouses} + isError={ + formik.touched.source_warehouse_id && + Boolean(formik.errors.source_warehouse_id) + } + errorMessage={formik.errors.source_warehouse_id as string} + isDisabled={type === 'detail'} + isClearable + startAdornment={ + formik.values.source_warehouse_id + ? getWarehouseStockAdornment( + formik.values.source_warehouse_id + ) + : undefined + } + /> + + {/* Area and Location Info */} +
+ - - {/* Area and Location Info */} -
- - -
-
-
- -
-
-

Gudang Tujuan

- { - formik.setFieldTouched('destination_warehouse', true); - formik.setFieldValue('destination_warehouse', val); - formik.setFieldTouched('destination_warehouse_id', true); - formik.setFieldValue( - 'destination_warehouse_id', - (val as WarehouseOptionType)?.value - ); + - - {/* Area and Location Info */} -
- - -
-
+ + + + { + formik.setFieldTouched('destination_warehouse', true); + formik.setFieldValue('destination_warehouse', val); + formik.setFieldTouched('destination_warehouse_id', true); + formik.setFieldValue( + 'destination_warehouse_id', + (val as WarehouseOptionType)?.value + ); + }} + options={warehouseOptions} + onInputChange={setWarehouseSelectInputValue} + isLoading={isLoadingWarehouses} + isError={ + formik.touched.destination_warehouse_id && + Boolean(formik.errors.destination_warehouse_id) + } + errorMessage={formik.errors.destination_warehouse_id as string} + isDisabled={type === 'detail'} + isClearable + startAdornment={ + formik.values.destination_warehouse_id + ? getWarehouseStockAdornment( + formik.values.destination_warehouse_id + ) + : undefined + } + /> + + {/* Area and Location Info */} +
+ + +
+
{/* Products table */} -
-
-

Produk

-
-
- - - {type !== 'detail' && ( -
- 0 + +
+ + + + {type !== 'detail' && ( + + )} + + + {type !== 'detail' && } + + + + {formik.values.products?.map((product, idx) => ( + + {type !== 'detail' && ( + + )} + + + {type !== 'detail' && ( + )} - - - {type !== 'detail' && } - - - {formik.values.products?.map((product, idx) => ( - - {type !== 'detail' && ( - - )} - - - {type !== 'detail' && ( - - )} - - ))} - -
+ 0 + } + onChange={( + e: React.ChangeEvent + ) => { + if (e.target.checked) { + setSelectedProducts( + formik.values.products?.map((_, idx) => idx) ?? + [] + ); + } else { + setSelectedProducts([]); } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> + + Produk + + * + + + Qty + + * + + Aksi
+ ) => { if (e.target.checked) { - setSelectedProducts( - formik.values.products?.map( - (_, idx) => idx - ) ?? [] - ); + setSelectedProducts([...selectedProducts, idx]); } else { - setSelectedProducts([]); + setSelectedProducts( + selectedProducts.filter((i) => i !== idx) + ); } }} classNames={{ @@ -985,221 +1040,262 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { checkbox: 'checkbox checkbox-sm', }} /> - + + { + formik.setFieldTouched( + `products.${idx}.product`, + true + ); + formik.setFieldValue( + `products.${idx}.product`, + val + ); + formik.setFieldTouched( + `products.${idx}.product_id`, + true + ); + formik.setFieldValue( + `products.${idx}.product_id`, + (val as ProductWarehouseOptionType)?.value + ); + }} + options={productWarehouseOptions} + onInputChange={setProductWarehouseSelectInputValue} + isLoading={isLoadingProductWarehouses} + isDisabled={ + type === 'detail' || + !formik.values.source_warehouse_id + } + placeholder={ + !formik.values.source_warehouse_id + ? 'Pilih gudang asal terlebih dahulu...' + : 'Pilih produk...' + } + isClearable + {...isRepeaterInputError( + 'products', + 'product_id', + idx + )} + className={{ + wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', + }} + /> + + + +
+ +
+
- Produk - - * - - - Qty - - * - - Aksi
- - ) => { - if (e.target.checked) { - setSelectedProducts([ - ...selectedProducts, - idx, - ]); - } else { - setSelectedProducts( - selectedProducts.filter((i) => i !== idx) - ); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> - - { - formik.setFieldTouched( - `products.${idx}.product`, - true - ); - formik.setFieldValue( - `products.${idx}.product`, - val - ); - formik.setFieldTouched( - `products.${idx}.product_id`, - true - ); - formik.setFieldValue( - `products.${idx}.product_id`, - (val as ProductWarehouseOptionType)?.value - ); - }} - options={productWarehouseOptions} - onInputChange={setProductWarehouseSelectInputValue} - isLoading={isLoadingProductWarehouses} - isDisabled={ - type === 'detail' || - !formik.values.source_warehouse_id - } - placeholder={ - !formik.values.source_warehouse_id - ? 'Pilih gudang asal terlebih dahulu...' - : 'Pilih produk...' - } - isClearable - {...isRepeaterInputError( - 'products', - 'product_id', - idx - )} - className={{ - wrapper: - 'w-full min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - - - -
- -
-
-
- {type !== 'detail' && ( -
- {selectedProducts.length > 0 && ( - - )} + ))} + +
+
+ {type !== 'detail' && ( +
+ {selectedProducts.length > 0 && ( -
- )} -
-
+ )} + +
+ )} + {/* Deliveries table */} -
-
-

Pengiriman

-
- - - - {type !== 'detail' && ( - + + + {type !== 'detail' && ( + + )} + + ))} + +
- 0 + +
+ + + + {type !== 'detail' && ( + + )} + + + + + + + + + {type !== 'detail' && } + + + + {formik.values.deliveries?.map((delivery, idx) => ( + + {type !== 'detail' && ( + )} - - - - - - - - - {type !== 'detail' && } - - - - {formik.values.deliveries?.map((delivery, idx) => ( - - {type !== 'detail' && ( - - )} - - - - - + + - - - - {type !== 'detail' && ( - + + + + ) : ( + { + const file = e.target.files?.[0]; + if (file) { + if (file.size > 2 * 1024 * 1024) { + toast.error('Ukuran dokumen maksimal 2 MB!'); + e.target.value = ''; + return; + } + formik.setFieldValue( + `deliveries.${idx}.document`, + file + ); + } + }} + {...isRepeaterInputError( + 'deliveries', + 'document', + idx + )} + className={{ + wrapper: + 'w-full min-w-72 md:w-min-80 lg:w-min-96', + }} + /> )} - - ))} - -
+ 0 + } + onChange={( + e: React.ChangeEvent + ) => { + if (e.target.checked) { + setSelectedDeliveries( + formik.values.deliveries?.map( + (_, idx) => idx + ) ?? [] + ); + } else { + setSelectedDeliveries([]); } + }} + classNames={{ + wrapper: 'flex justify-center', + checkbox: 'checkbox checkbox-sm', + }} + /> + + Produk + + * + + + Qty + + * + + + Supplier + + * + + + Plat Nomor + + * + + Dokumen + Biaya Pengiriman (Rp.) + + * + + + Biaya Per Item (Rp.) + + * + + + Nama Sopir + + * + + Aksi
+ ) => { if (e.target.checked) { - setSelectedDeliveries( - formik.values.deliveries?.map( - (_, idx) => idx - ) ?? [] - ); + setSelectedDeliveries([ + ...selectedDeliveries, + idx, + ]); } else { - setSelectedDeliveries([]); + setSelectedDeliveries( + selectedDeliveries.filter((i) => i !== idx) + ); } }} classNames={{ @@ -1207,417 +1303,307 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => { checkbox: 'checkbox checkbox-sm', }} /> - + - Produk - - * - - - Qty - - * - - - Supplier - - * - - - Plat Nomor - - * - - Dokumen - Biaya Pengiriman (Rp.) - - * - - - Biaya Per Item (Rp.) - - * - - - Nama Sopir - - * - - Aksi
- - ) => { - if (e.target.checked) { - setSelectedDeliveries([ - ...selectedDeliveries, - idx, - ]); - } else { - setSelectedDeliveries( - selectedDeliveries.filter((i) => i !== idx) - ); - } - }} - classNames={{ - wrapper: 'flex justify-center', - checkbox: 'checkbox checkbox-sm', - }} - /> - - { - formik.setFieldTouched( - `deliveries.${idx}.products.0.product`, - true - ); - formik.setFieldValue( - `deliveries.${idx}.products.0.product`, - val - ); - formik.setFieldTouched( - `deliveries.${idx}.products.0.product_id`, - true - ); - formik.setFieldValue( - `deliveries.${idx}.products.0.product_id`, - (val as OptionType)?.value - ); - }} - options={getFilteredProductWarehouseOptions()} - isDisabled={type === 'detail'} - isClearable - isError={ - isDeliveryProductInputError(idx, 0, 'product_id') - .isError - } - errorMessage={ - isDeliveryProductInputError(idx, 0, 'product_id') - .errorMessage - } - className={{ - wrapper: - 'w-full min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - - - - { - formik.setFieldTouched( - `deliveries.${idx}.supplier`, - true - ); - formik.setFieldValue( - `deliveries.${idx}.supplier`, - val - ); - formik.setFieldTouched( - `deliveries.${idx}.supplier_id`, - true - ); - formik.setFieldValue( - `deliveries.${idx}.supplier_id`, - (val as OptionType)?.value - ); - }} - options={supplierOptions} - onInputChange={setSupplierSelectInputValue} - isLoading={isLoadingSuppliers} - isDisabled={type === 'detail'} - isClearable - {...isRepeaterInputError( - 'deliveries', - 'supplier_id', - idx - )} - className={{ - wrapper: - 'w-full min-w-52 md:min-w-72 lg:min-w-80', - }} - /> - - - - {type === 'detail' ? ( - <> -
- -
- - ) : ( - { - const file = e.target.files?.[0]; - if (file) { - if (file.size > 2 * 1024 * 1024) { - toast.error( - 'Ukuran dokumen maksimal 2 MB!' - ); - e.target.value = ''; - return; - } - formik.setFieldValue( - `deliveries.${idx}.document`, - file - ); - } - }} - {...isRepeaterInputError( - 'deliveries', - 'document', - idx - )} - className={{ - wrapper: - 'w-full min-w-72 md:w-min-80 lg:w-min-96', - }} - /> +
+ { + formik.setFieldTouched( + `deliveries.${idx}.products.0.product`, + true + ); + formik.setFieldValue( + `deliveries.${idx}.products.0.product`, + val + ); + formik.setFieldTouched( + `deliveries.${idx}.products.0.product_id`, + true + ); + formik.setFieldValue( + `deliveries.${idx}.products.0.product_id`, + (val as OptionType)?.value + ); + }} + options={getFilteredProductWarehouseOptions()} + isDisabled={type === 'detail'} + isClearable + isError={ + isDeliveryProductInputError(idx, 0, 'product_id') + .isError + } + errorMessage={ + isDeliveryProductInputError(idx, 0, 'product_id') + .errorMessage + } + className={{ + wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', + }} + /> + + + + { + formik.setFieldTouched( + `deliveries.${idx}.supplier`, + true + ); + formik.setFieldValue( + `deliveries.${idx}.supplier`, + val + ); + formik.setFieldTouched( + `deliveries.${idx}.supplier_id`, + true + ); + formik.setFieldValue( + `deliveries.${idx}.supplier_id`, + (val as OptionType)?.value + ); + }} + options={supplierOptions} + onInputChange={setSupplierSelectInputValue} + isLoading={isLoadingSuppliers} + isDisabled={type === 'detail'} + isClearable + {...isRepeaterInputError( + 'deliveries', + 'supplier_id', + idx )} - - - - - - - + className={{ + wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80', + }} + /> + + + + {type === 'detail' ? ( + <>
-
-
- {type !== 'detail' && ( -
- {selectedDeliveries.length > 0 && ( - - )} + +
+ + + + + + +
+ +
+
+
+ {type !== 'detail' && ( +
+ {selectedDeliveries.length > 0 && ( -
- )} -
-
+ )} + +
+ )} + {/* Action buttons */}
From b19be7dd4b1f20c4fcc982d0bc3b31ebcbec5958 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 4 Nov 2025 14:53:07 +0700 Subject: [PATCH 029/101] refactor(FE-174): update recording types to include approval and egg grading fields for enhanced data handling --- src/types/api/production/recording.d.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 5b544175..2fa2fa81 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -1,4 +1,4 @@ -import { BaseMetadata, User } from '@/types/api/api-general'; +import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general'; export type ProductionMetrics = { total_depletion_qty: number; @@ -8,13 +8,13 @@ export type ProductionMetrics = { cum_intake: number; fcr_value: number; total_chick_qty: number; - daily_depletion_rate: number; - cum_depletion: number; + daily_depletion_rate?: number; + cum_depletion?: number; }; export type BaseRecording = { id: number; - project_flock_kandangs_id: number; + project_flock_kandang_id: number; record_datetime: string; day: number; created_by: User; @@ -61,6 +61,11 @@ export type GradingEgg = { export type Recording = BaseMetadata & BaseRecording & { + project_flock_category?: 'GROWING' | 'LAYING'; + approval?: BaseApproval; + egg_grading_status?: string | null; + egg_grading_pending_qty?: number | null; + egg_grading_completed_qty?: number | null; recording_bws?: RecordingBW[]; recording_depletions?: RecordingDepletion[]; recording_stocks?: RecordingStock[]; From 0a17249fb9aa9b03e58a36277952f3c098170690 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 4 Nov 2025 14:54:07 +0700 Subject: [PATCH 030/101] refactor(FE-174): rename project_flock_kandangs_id to project_flock_kandang_id for consistency in RecordingForm schema --- .../recording/form/RecordingForm.schema.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 6660e491..47fad11e 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -12,7 +12,7 @@ type RecordingGrowingFormSchemaType = { value: number; label: string; } | null; - project_flock_kandangs_id: number; + project_flock_kandang_id: number; body_weights: { weight: number | string; avg_weight: number | string; @@ -118,7 +118,7 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema & { export const getRecordingGrowingFormInitialValues = ( initialValues?: RecordingFormData ): RecordingGrowingFormValues => ({ - project_flock_kandang: initialValues?.project_flock_kandangs_id + project_flock_kandang: initialValues?.project_flock_kandang_id ? { - value: initialValues.project_flock_kandangs_id, - label: `Project Flock #${initialValues.project_flock_kandangs_id}`, + value: initialValues.project_flock_kandang_id, + label: `Project Flock #${initialValues.project_flock_kandang_id}`, } : null, - project_flock_kandangs_id: initialValues?.project_flock_kandangs_id ?? 0, + project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, body_weights: initialValues?.body_weights?.map( (bw: NonNullable[0]) => ({ weight: bw.avg_weight * bw.qty, From 966ad7545c5f728dd8a114a55952ca0d7c573ff7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 4 Nov 2025 15:20:33 +0700 Subject: [PATCH 031/101] refactor(FE-174): rename project_flock_kandangs_id to project_flock_kandang_id for consistency in RecordingForm --- .../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 30aee0ac..e3a975b2 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -157,7 +157,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { project_flock_id: selectedProjectFlock.value.toString(), kandang_id: selectedKandang.value.toString(), }); - return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; + return `${ProjectFlockApi.basePath}/kandang/lookup?${params.toString()}`; }, [selectedProjectFlock, selectedKandang]); const { data: projectFlockKandangLookupData } = useSWR( @@ -272,7 +272,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { todayRecordings.forEach((recording) => { const recordingDate = recording.record_datetime?.split('T')[0]; if (recordingDate === today) { - recordedIds.add(recording.project_flock_kandangs_id); + recordedIds.add(recording.project_flock_kandang_id); } }); @@ -419,7 +419,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const layingValues = values as RecordingLayingFormValues; const layingPayload = { - project_flock_kandang_id: layingValues.project_flock_kandangs_id, + project_flock_kandang_id: layingValues.project_flock_kandang_id, body_weights: (layingValues.body_weights ?? []).map((bw) => ({ avg_weight: typeof bw.avg_weight === 'number' @@ -458,7 +458,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const growingValues = values as RecordingGrowingFormValues; const growingPayload = { - project_flock_kandang_id: growingValues.project_flock_kandangs_id, + project_flock_kandang_id: growingValues.project_flock_kandang_id, body_weights: (growingValues.body_weights ?? []).map((bw) => ({ avg_weight: typeof bw.avg_weight === 'number' @@ -688,20 +688,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { setSelectedProjectFlock(null); setSelectedKandang(null); formik.setFieldValue('project_flock_kandang', null); - formik.setFieldValue('project_flock_kandangs_id', 0); + formik.setFieldValue('project_flock_kandang_id', 0); }; const projectFlockChangeHandler = (val: OptionType | OptionType[] | null) => { setSelectedProjectFlock(val as OptionType); setSelectedKandang(null); formik.setFieldValue('project_flock_kandang', null); - formik.setFieldValue('project_flock_kandangs_id', 0); + formik.setFieldValue('project_flock_kandang_id', 0); }; const kandangChangeHandler = (val: OptionType | OptionType[] | null) => { setSelectedKandang(val as OptionType); formik.setFieldTouched('project_flock_kandang', true); - formik.setFieldTouched('project_flock_kandangs_id', true); + formik.setFieldTouched('project_flock_kandang_id', true); }; useEffect(() => { @@ -717,7 +717,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return; } - formik.setFieldValue('project_flock_kandangs_id', projectFlockKandangId); + formik.setFieldValue('project_flock_kandang_id', projectFlockKandangId); formik.setFieldValue('project_flock_kandang', { value: projectFlockKandangId, From 77e3fe12c325db495b927027e03a6c5c61d52908 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 4 Nov 2025 16:06:04 +0700 Subject: [PATCH 032/101] refactor(FE-170): update API endpoint and rename label for project flock in RecordingForm --- .../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 e3a975b2..eedbec03 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -157,7 +157,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { project_flock_id: selectedProjectFlock.value.toString(), kandang_id: selectedKandang.value.toString(), }); - return `${ProjectFlockApi.basePath}/kandang/lookup?${params.toString()}`; + return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; }, [selectedProjectFlock, selectedKandang]); const { data: projectFlockKandangLookupData } = useSWR( @@ -242,7 +242,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { return ( projectFlocks?.data.map((projectFlock) => ({ value: projectFlock.id, - label: projectFlock.flock.name, + label: projectFlock.flock_name, })) || [] ); }, [projectFlocks]); From b0665b25418bcd6f1e34d89f7de4994b4c9bb314 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 4 Nov 2025 16:06:39 +0700 Subject: [PATCH 033/101] refactor(FE-174): rename name to flock_name in BaseProjectFlock type for clarity --- src/types/api/production/project-flock.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index cedfca38..f9bc3c38 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -7,7 +7,7 @@ import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; export type BaseProjectFlock = { id: number; - name: string; + flock_name: string; status: string; flock: Flock; flock_id: number; From b7ab537b95f60c54e9bb72270c28e880597828ba Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 4 Nov 2025 16:07:02 +0700 Subject: [PATCH 034/101] feat(FE-170): add selection checkboxes and enhance project details in RecordingTable --- .../production/recording/RecordingTable.tsx | 68 +++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 722589c8..4364f47e 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { SortingState, CellContext } from '@tanstack/react-table'; -import { cn } from '@/lib/helper'; +import { cn, formatDate } from '@/lib/helper'; import { useModal } from '@/components/Modal'; import Button from '@/components/Button'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; @@ -24,6 +24,8 @@ import { KandangApi } from '@/services/api/master-data'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import toast from 'react-hot-toast'; +import Badge from '@/components/Badge'; +import CheckboxInput from '@/components/input/CheckboxInput'; const RowOptionsMenu = ({ type = 'dropdown', @@ -325,6 +327,30 @@ const RecordingTable = () => { data={isResponseSuccess(recordings) ? recordings?.data : []} columns={[ + { + id: 'select', + header: ({ table }) => ( +
+ +
+ ), + cell: ({ row }) => ( +
+ +
+ ), + }, { header: '#', cell: (props) => @@ -335,7 +361,20 @@ const RecordingTable = () => { { header: 'Nama Project', cell: (props) => - `Project ${props.row.original.project_flock_kandangs_id}`, + `Project ${props.row.original.project_flock_kandang_id}`, + }, + { + header: 'Kategori', + cell: (props) => { + const category = props.row.original.project_flock_category; + if (!category) return '-'; + const color = category === 'LAYING' ? 'info' : 'warning'; + return ( + + {category} + + ); + }, }, { header: 'Umur (hari)', @@ -345,17 +384,38 @@ const RecordingTable = () => { accessorKey: 'record_date', header: 'Waktu Recording', cell: (props) => - new Date(props.row.original.record_datetime).toLocaleDateString(), + formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'), }, { header: 'Populasi Awal', cell: (props) => props.row.original.total_chick_qty?.toLocaleString() || '-', }, + { + header: 'Status Approval', + cell: (props) => props.row.original.approval?.step_name || '-', + }, + { + header: 'Status Grading Telur', + cell: (props) => { + const status = props.row.original.egg_grading_status; + if (!status) return '-'; + const color = status === 'COMPLETED' ? 'success' : 'warning'; + return ( + + {status} + + ); + }, + }, + { + header: 'Dibuat Oleh', + cell: (props) => props.row.original.created_user?.name || '-', + }, { header: 'Tanggal Submit', cell: (props) => - new Date(props.row.original.created_at).toLocaleString(), + formatDate(props.row.original.created_at, 'DD MMMM YYYY'), }, { header: 'Aksi', From 04a1f5e014022de24426b6e9804796c8554961fe Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 4 Nov 2025 16:23:29 +0700 Subject: [PATCH 035/101] feat(FE-170,174): add total_weight field and update calculations in body_weights for RecordingForm --- .../recording/form/RecordingForm.schema.ts | 8 +++ .../recording/form/RecordingForm.tsx | 55 ++++++++++++++----- src/types/api/production/recording.d.ts | 1 + 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/components/pages/production/recording/form/RecordingForm.schema.ts b/src/components/pages/production/recording/form/RecordingForm.schema.ts index 47fad11e..383d4164 100644 --- a/src/components/pages/production/recording/form/RecordingForm.schema.ts +++ b/src/components/pages/production/recording/form/RecordingForm.schema.ts @@ -17,6 +17,7 @@ type RecordingGrowingFormSchemaType = { weight: number | string; avg_weight: number | string; qty: number | string; + total_weight: number | string; }[]; stocks: { product_warehouse_id: number; @@ -47,6 +48,7 @@ export type BodyWeightSchema = { weight: number | string; avg_weight: number | string; qty: number | string; + total_weight: number | string; }; export type StockSchema = { @@ -77,6 +79,10 @@ const BodyWeightObjectSchema: Yup.ObjectSchema = Yup.object({ .required('Jumlah ayam wajib diisi!') .min(1, 'Jumlah ayam minimal 1 ekor!') .typeError('Jumlah ayam harus berupa angka!'), + total_weight: Yup.number() + .required('Berat ayam total wajib diisi!') + .min(0, 'Berat ayam total tidak boleh negatif!') + .typeError('Berat ayam total harus berupa angka!'), }); const StockObjectSchema: Yup.ObjectSchema = Yup.object({ @@ -249,12 +255,14 @@ export const getRecordingGrowingFormInitialValues = ( weight: bw.avg_weight * bw.qty, avg_weight: bw.avg_weight, qty: bw.qty, + total_weight: bw.total_weight, }) ) ?? [ { weight: '', avg_weight: '', qty: '', + total_weight: 0, }, ], stocks: initialValues?.stocks?.map( diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index eedbec03..a0ade3fb 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -420,13 +420,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const layingPayload = { project_flock_kandang_id: layingValues.project_flock_kandang_id, - body_weights: (layingValues.body_weights ?? []).map((bw) => ({ - avg_weight: - typeof bw.avg_weight === 'number' - ? bw.avg_weight - : parseFloat(String(bw.avg_weight)) || 0, - qty: Number(bw.qty) || 0, - })), + body_weights: (layingValues.body_weights ?? []).map((bw) => { + const qty = Number(bw.qty) || 0; + const weight = Number(bw.weight) || 0; + const totalWeight = qty * weight; + + return { + avg_weight: + typeof bw.avg_weight === 'number' + ? bw.avg_weight + : parseFloat(String(bw.avg_weight)) || 0, + qty: qty, + total_weight: parseFloat(String(totalWeight)) || 0, + }; + }), stocks: (layingValues.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, qty: Number(stock.qty) || 0, @@ -459,13 +466,20 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const growingPayload = { project_flock_kandang_id: growingValues.project_flock_kandang_id, - body_weights: (growingValues.body_weights ?? []).map((bw) => ({ - avg_weight: - typeof bw.avg_weight === 'number' - ? bw.avg_weight - : parseFloat(String(bw.avg_weight)) || 0, - qty: Number(bw.qty) || 0, - })), + body_weights: (growingValues.body_weights ?? []).map((bw) => { + const qty = Number(bw.qty) || 0; + const weight = Number(bw.weight) || 0; + const totalWeight = qty * weight; + + return { + avg_weight: + typeof bw.avg_weight === 'number' + ? bw.avg_weight + : parseFloat(String(bw.avg_weight)) || 0, + qty: qty, + total_weight: parseFloat(String(totalWeight)) || 0, + }; + }), stocks: (growingValues.stocks ?? []).map((stock) => ({ product_warehouse_id: stock.product_warehouse_id, qty: Number(stock.qty) || 0, @@ -801,12 +815,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const currentWeight = formik.values.body_weights?.[idx]; if (currentWeight) { const qty = Number(currentWeight.qty) || 0; + const totalWeight = qty * value; + if (qty > 0 && value > 0) { const avgWeight = parseFloat((value / qty).toFixed(2)); formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight); } else { formik.setFieldValue(`body_weights.${idx}.avg_weight`, 0); } + + // Update total_weight + formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight); } }; @@ -825,8 +844,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { if (qty > 0 && value > 0) { const totalWeight = value * qty; formik.setFieldValue(`body_weights.${idx}.weight`, totalWeight); + // Update total_weight + formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight); } else { formik.setFieldValue(`body_weights.${idx}.weight`, 0); + formik.setFieldValue(`body_weights.${idx}.total_weight`, 0); } } }; @@ -843,12 +865,17 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { const currentWeight = formik.values.body_weights?.[idx]; if (currentWeight) { const weight = Number(currentWeight.weight) || 0; + const totalWeight = value * weight; + if (value > 0 && weight > 0) { const avgWeight = parseFloat((weight / value).toFixed(2)); formik.setFieldValue(`body_weights.${idx}.avg_weight`, avgWeight); } else { formik.setFieldValue(`body_weights.${idx}.avg_weight`, 0); } + + // Update total_weight + formik.setFieldValue(`body_weights.${idx}.total_weight`, totalWeight); } }; diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 2fa2fa81..7977d130 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -78,6 +78,7 @@ export type CreateGrowingRecordingPayload = { body_weights: { avg_weight: number; qty: number; + total_weight: number; }[]; stocks?: { product_warehouse_id: number; From 02cc4a759d388c9029d5faa0f6744e5b937ad0e8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 5 Nov 2025 08:43:10 +0700 Subject: [PATCH 036/101] feat(FE-Storyless): add approval workflows for project flocks and recordings --- src/config/constant.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index b5a12fb4..ff5ada89 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -225,3 +225,36 @@ export const RECORDING_FLAG_OPTIONS = [ { label: 'Ayam Culling', value: 'Culling' }, { label: 'Ayam Mati', value: 'Mati' }, ]; + +export const APPROVAL_WORKFLOWS = [ + { + key: 'PROJECT_FLOCKS', + steps: [ + { + step_number: 1, + step_name: 'Pengajuan', + }, + { + step_number: 2, + step_name: 'Aktif', + }, + ], + }, + { + key: 'RECORDINGS', + steps: [ + { + step_number: 1, + step_name: 'Grading-Telur', + }, + { + step_number: 2, + step_name: 'Pengajuan', + }, + { + step_number: 3, + step_name: 'Disetujui', + }, + ], + }, +]; From fa36c10c01f76a5c79dc10c56314a7e745882314 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 5 Nov 2025 09:46:38 +0700 Subject: [PATCH 037/101] feat(FE-170,175): add approval and rejection functionality with confirmation modals in RecordingTable --- src/components/modal/ConfirmationModal.tsx | 8 + .../production/recording/RecordingTable.tsx | 152 +++++++++++++++++- 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/src/components/modal/ConfirmationModal.tsx b/src/components/modal/ConfirmationModal.tsx index 04c221e6..3fddba42 100644 --- a/src/components/modal/ConfirmationModal.tsx +++ b/src/components/modal/ConfirmationModal.tsx @@ -30,6 +30,7 @@ interface ConfirmationModalProps { modal?: string; modalBox?: string; }; + children?: React.ReactNode; } const ConfirmationModal = ({ @@ -40,6 +41,7 @@ const ConfirmationModal = ({ primaryButton, secondaryButton, className, + children, }: ConfirmationModalProps) => { const closeModalHandler = () => { ref.current?.close(); @@ -90,6 +92,12 @@ const ConfirmationModal = ({ {text ?? 'Apakah anda yakin ingin melakukan hal ini?'}

+ {children && ( +
+ {children} +
+ )} +
{secondaryButton && secondaryButton.text && ( + +