diff --git a/src/app/expense/realization/page.tsx b/src/app/expense/realization/page.tsx index 027e8d65..50b64e11 100644 --- a/src/app/expense/realization/page.tsx +++ b/src/app/expense/realization/page.tsx @@ -37,7 +37,7 @@ const ExpenseRealization = () => { const isExpenseCanBeRealized = isResponseSuccess(expense) && expense.data.latest_approval.action !== 'REJECTED' && - expense.data.latest_approval.step_number === 3; + expense.data.latest_approval.step_number === 4; if (isResponseSuccess(expense) && !isExpenseCanBeRealized) { if (typeof window !== 'undefined') { diff --git a/src/components/pages/expense/ExpenseDetail.tsx b/src/components/pages/expense/ExpenseDetail.tsx index 859b19ce..9c84ed4d 100644 --- a/src/components/pages/expense/ExpenseDetail.tsx +++ b/src/components/pages/expense/ExpenseDetail.tsx @@ -28,7 +28,7 @@ const ExpenseDetail: React.FC = ({ initialValues }) => { if ( initialValues?.latest_approval && - initialValues?.latest_approval.step_number >= 4 && + initialValues?.latest_approval.step_number >= 5 && initialValues.latest_approval.action !== 'REJECTED' ) { validTabs.push({ diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index 657c5e5c..82c58341 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -59,34 +59,40 @@ const ExpenseRequestContent = ({ const isLatestApprovalRejectedOrDone = isLatestApprovalRejected || - initialValues?.latest_approval.step_number === 5; + initialValues?.latest_approval.step_number === 6; - const isCurrentApprovalOnManager = + const isCurrentApprovalOnHeadArea = !isLatestApprovalRejected && initialValues?.latest_approval.step_number === 1; - const isCurrentApprovalOnFinance = + const isCurrentApprovalOnUnitVicePresident = !isLatestApprovalRejected && initialValues?.latest_approval.step_number === 2; + const isCurrentApprovalOnFinance = + !isLatestApprovalRejected && + initialValues?.latest_approval.step_number === 3; + const isCurrentApprovalOnRealization = !isLatestApprovalRejected && - initialValues?.latest_approval.step_number === 4; + initialValues?.latest_approval.step_number === 5; const showEditButton = - initialValues?.latest_approval.step_number !== 5 && + initialValues?.latest_approval.step_number !== 6 && (initialValues?.latest_approval.step_number === 1 || initialValues?.latest_approval.step_number === 2 || - initialValues?.latest_approval.step_number === 3); + initialValues?.latest_approval.step_number === 3 || + initialValues?.latest_approval.step_number === 4); const showRejectButton = !isLatestApprovalRejected && (initialValues?.latest_approval.step_number === 1 || - initialValues?.latest_approval.step_number === 2); + initialValues?.latest_approval.step_number === 2 || + initialValues?.latest_approval.step_number === 3); const isExpenseCanBeRealized = !isLatestApprovalRejected && - initialValues?.latest_approval.step_number === 3; + initialValues?.latest_approval.step_number === 4; // Modal hooks const deleteModal = useModal(); @@ -174,8 +180,15 @@ const ExpenseRequestContent = ({ let approveResponse: BaseApiResponse | undefined = undefined; - if (isCurrentApprovalOnManager) { - approveResponse = await ExpenseApi.approveManager( + if (isCurrentApprovalOnHeadArea) { + approveResponse = await ExpenseApi.approveHeadArea( + initialValues.id, + notes + ); + } + + if (isCurrentApprovalOnUnitVicePresident) { + approveResponse = await ExpenseApi.approveUnitVicePresident( initialValues.id, notes ); @@ -207,8 +220,15 @@ const ExpenseRequestContent = ({ let rejectResponse: BaseApiResponse | undefined = undefined; - if (isCurrentApprovalOnManager) { - rejectResponse = await ExpenseApi.rejectManager(initialValues.id, notes); + if (isCurrentApprovalOnHeadArea) { + rejectResponse = await ExpenseApi.rejectHeadArea(initialValues.id, notes); + } + + if (isCurrentApprovalOnUnitVicePresident) { + rejectResponse = await ExpenseApi.rejectUnitVicePresident( + initialValues.id, + notes + ); } if (isCurrentApprovalOnFinance) { @@ -255,8 +275,8 @@ const ExpenseRequestContent = ({ {/* TODO: apply RBAC */}
- {isCurrentApprovalOnManager && ( - + {isCurrentApprovalOnHeadArea && ( + + + )} + + {isCurrentApprovalOnUnitVicePresident && ( + + )} @@ -304,7 +338,8 @@ const ExpenseRequestContent = ({ {showRejectButton && ( @@ -454,8 +489,8 @@ const ExpenseRequestContent = ({ : {formatCurrency( - initialValues?.latest_approval.step_number === 4 || - initialValues?.latest_approval.step_number === 5 + initialValues?.latest_approval.step_number === 5 || + initialValues?.latest_approval.step_number === 6 ? (initialValues?.total_realisasi ?? 0) : (initialValues?.total_pengajuan ?? 0) )} diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 1f3e9df5..fdfd9cc3 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -55,15 +55,16 @@ const RowOptionsMenu = ({ deleteClickHandler: () => void; }) => { const showEditButton = - props.row.original.latest_approval.step_number !== 5 && + props.row.original.latest_approval.step_number !== 6 && (props.row.original.latest_approval.step_number === 1 || props.row.original.latest_approval.step_number === 2 || - props.row.original.latest_approval.step_number === 3); + props.row.original.latest_approval.step_number === 3 || + props.row.original.latest_approval.step_number === 4); // TODO: apply RBAC const showRealizationButton = props.row.original.latest_approval.action !== 'REJECTED' && - props.row.original.latest_approval.step_number === 3; + props.row.original.latest_approval.step_number === 4; return ( @@ -193,7 +194,7 @@ const ExpensesTable = () => { parseInt(item) ); - const isAllSelectedRowLatestApprovalOnManager = useMemo(() => { + const isAllSelectedRowLatestApprovalOnHeadArea = useMemo(() => { return selectedRowIds.every((rowId) => { if (!isResponseSuccess(expenses)) return false; @@ -202,11 +203,28 @@ const ExpensesTable = () => { const isLatestApprovalRejected = expenseItem?.latest_approval.action === 'REJECTED'; - const isCurrentApprovalOnManager = + const isCurrentApprovalOnHeadArea = !isLatestApprovalRejected && expenseItem?.latest_approval.step_number === 1; - return isCurrentApprovalOnManager; + return isCurrentApprovalOnHeadArea; + }); + }, [expenses, selectedRowIds]); + + const isAllSelectedRowLatestApprovalOnUnitVicePresident = useMemo(() => { + return selectedRowIds.every((rowId) => { + if (!isResponseSuccess(expenses)) return false; + + const expenseItem = expenses.data.find((item) => item.id === rowId); + + const isLatestApprovalRejected = + expenseItem?.latest_approval.action === 'REJECTED'; + + const isCurrentApprovalOnUnitVicePresident = + !isLatestApprovalRejected && + expenseItem?.latest_approval.step_number === 2; + + return isCurrentApprovalOnUnitVicePresident; }); }, [expenses, selectedRowIds]); @@ -221,7 +239,7 @@ const ExpensesTable = () => { const isCurrentApprovalOnFinance = !isLatestApprovalRejected && - expenseItem?.latest_approval.step_number === 2; + expenseItem?.latest_approval.step_number === 3; return isCurrentApprovalOnFinance; }); @@ -238,7 +256,7 @@ const ExpensesTable = () => { const isCurrentApprovalOnRealization = !isLatestApprovalRejected && - expenseItem?.latest_approval.step_number === 4; + expenseItem?.latest_approval.step_number === 5; return isCurrentApprovalOnRealization; }); @@ -397,7 +415,7 @@ const ExpensesTable = () => { ) => { return ( row.original.latest_approval.action !== 'REJECTED' && - row.original.latest_approval.step_number !== 5 + row.original.latest_approval.step_number !== 6 ); }; @@ -441,8 +459,13 @@ const ExpensesTable = () => { let bulkApproveResponse: BaseApiResponse | undefined = undefined; - if (isAllSelectedRowLatestApprovalOnManager) { - bulkApproveResponse = await ExpenseApi.bulkApproveManager( + if (isAllSelectedRowLatestApprovalOnHeadArea) { + bulkApproveResponse = await ExpenseApi.bulkApproveHeadArea( + selectedRowIds, + notes + ); + } else if (isAllSelectedRowLatestApprovalOnUnitVicePresident) { + bulkApproveResponse = await ExpenseApi.bulkApproveUnitVicePresident( selectedRowIds, notes ); @@ -478,8 +501,13 @@ const ExpensesTable = () => { let bulkRejectResponse: BaseApiResponse | undefined = undefined; - if (isAllSelectedRowLatestApprovalOnManager) { - bulkRejectResponse = await ExpenseApi.bulkRejectManager( + if (isAllSelectedRowLatestApprovalOnHeadArea) { + bulkRejectResponse = await ExpenseApi.bulkRejectHeadArea( + selectedRowIds, + notes + ); + } else if (isAllSelectedRowLatestApprovalOnUnitVicePresident) { + bulkRejectResponse = await ExpenseApi.bulkRejectUnitVicePresident( selectedRowIds, notes ); @@ -594,16 +622,31 @@ const ExpensesTable = () => { {selectedRowIds.length > 0 && ( <> - + + + + + @@ -622,7 +665,8 @@ const ExpensesTable = () => { @@ -631,7 +675,8 @@ const ExpensesTable = () => { color='error' onClick={bulkRejectClickHandler} disabled={ - !isAllSelectedRowLatestApprovalOnManager && + !isAllSelectedRowLatestApprovalOnHeadArea && + !isAllSelectedRowLatestApprovalOnUnitVicePresident && !isAllSelectedRowLatestApprovalOnFinance } className='w-full sm:w-fit' diff --git a/src/components/pages/expense/RealizationStatusBadge.tsx b/src/components/pages/expense/RealizationStatusBadge.tsx index e042c022..720c1d03 100644 --- a/src/components/pages/expense/RealizationStatusBadge.tsx +++ b/src/components/pages/expense/RealizationStatusBadge.tsx @@ -9,7 +9,7 @@ interface RealizationStatusBadgeProps { const RealizationStatusBadge = ({ approval }: RealizationStatusBadgeProps) => { const isLatestApprovalRejected = approval?.action === 'REJECTED'; - const isExpenseRealized = approval?.step_number && approval.step_number >= 4; + const isExpenseRealized = approval?.step_number && approval.step_number >= 5; const realizationStatus = isExpenseRealized ? 'Sudah Realisasi' diff --git a/src/components/pages/expense/form/ExpenseRequestForm.schema.ts b/src/components/pages/expense/form/ExpenseRequestForm.schema.ts index 71357361..9eff1d26 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.schema.ts +++ b/src/components/pages/expense/form/ExpenseRequestForm.schema.ts @@ -75,7 +75,16 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = deleted_documents: Yup.array().of(Yup.number().required()).optional(), - documents: Yup.array().of(Yup.mixed().required()).optional(), + documents: Yup.array() + .of( + Yup.mixed() + .required() + .test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => { + if (!value || !(value instanceof File)) return true; + return value.size <= 5 * 1024 * 1024; + }) + ) + .optional(), expense_nonstocks: Yup.array() .of( @@ -104,7 +113,16 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = export const UpdateExpenseRequestFormSchema = ExpenseRequestFormSchema; export const UploadRequestDocumentsFormSchema = Yup.object({ - documents: Yup.array().of(Yup.mixed().required()).required(), + documents: Yup.array() + .of( + Yup.mixed() + .required() + .test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => { + if (!value || !(value instanceof File)) return true; + return value.size <= 5 * 1024 * 1024; + }) + ) + .required(), }); export type ExpenseRequestFormValues = Yup.InferType< diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx index 60e55397..ebd1b066 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.tsx +++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx @@ -37,6 +37,8 @@ import { cn, sleep } from '@/lib/helper'; import { LocationApi, SupplierApi } from '@/services/api/master-data'; import { ACCEPTED_FILE_TYPE } from '@/config/constant'; import { Supplier } from '@/types/api/master-data/supplier'; +import { getUniqueFormikErrors } from '@/lib/formik-helper'; +import AlertErrorList from '@/components/helper/form/FormErrors'; interface ExpenseFormProps { type?: 'add' | 'edit' | 'detail'; @@ -55,6 +57,7 @@ const ExpenseRequestForm = ({ const rejectModal = useModal(); const [expenseFormErrorMessage, setExpenseFormErrorMessage] = useState(''); + const [formErrorList, setFormErrorList] = useState([]); const createExpenseHandler = useCallback( async (payload: CreateExpensePayload) => { @@ -322,6 +325,22 @@ const ExpenseRequestForm = ({ router.push('/expense'); }; + const handleValidateForm = async () => { + const errors = await formik.validateForm(); + + if (Object.keys(errors).length > 0) { + const errorMessages = getUniqueFormikErrors(errors); + setFormErrorList(errorMessages); + return; + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleValidateForm(); + formik.handleSubmit(e); + }; + useEffect(() => { formikSetValues(getExpenseFormInitialValues(initialValues)); }, [formikSetValues, getExpenseFormInitialValues, initialValues]); @@ -347,10 +366,27 @@ const ExpenseRequestForm = ({
+ {expenseFormErrorMessage && ( +
+ + {expenseFormErrorMessage} +
+ )} + + {formErrorList.length > 0 && ( + setFormErrorList([])} + /> + )}
)} - {expenseFormErrorMessage && ( -
- - {expenseFormErrorMessage} -
- )} - {type !== 'detail' && (
{ expense?.latest_approval?.action === 'REJECTED'; const isExpenseRealized = expense?.latest_approval?.step_number && - expense?.latest_approval.step_number >= 4; + expense?.latest_approval.step_number >= 5; const realizationStatus = isExpenseRealized ? 'Sudah Realisasi' @@ -242,8 +242,8 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { { label: 'Nominal Biaya', value: formatCurrency( - expense?.latest_approval.step_number === 4 || - expense?.latest_approval.step_number === 5 + expense?.latest_approval.step_number === 5 || + expense?.latest_approval.step_number === 6 ? (expense?.total_realisasi ?? 0) : (expense?.total_pengajuan ?? 0) ), diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 4a9d6c13..4966172c 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -1737,16 +1737,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Egg Mass - {initialValues.egg_mesh && - initialValues.egg_mesh > 0 - ? formatNumber(initialValues.egg_mesh) + {initialValues.egg_mass && + initialValues.egg_mass > 0 + ? formatNumber(initialValues.egg_mass) : '-'} - {initialValues.egg_mesh_std && - initialValues.egg_mesh_std > 0 - ? formatNumber(initialValues.egg_mesh_std) + {initialValues.egg_mass_std && + initialValues.egg_mass_std > 0 + ? formatNumber(initialValues.egg_mass_std) : '-'} @@ -1773,16 +1773,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Hen Day - {initialValues.hand_day && - initialValues.hand_day > 0 - ? formatNumber(initialValues.hand_day) + {initialValues.hen_day && + initialValues.hen_day > 0 + ? formatNumber(initialValues.hen_day) : '-'} - {initialValues.hand_day_std !== undefined && - initialValues.hand_day_std > 0 - ? `${initialValues.hand_day_std}%` + {initialValues.hen_day_std !== undefined && + initialValues.hen_day_std > 0 + ? `${initialValues.hen_day_std}%` : '-'} @@ -1790,16 +1790,16 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { Hen House - {initialValues.hand_house && - initialValues.hand_house > 0 - ? formatNumber(initialValues.hand_house) + {initialValues.hen_house && + initialValues.hen_house > 0 + ? formatNumber(initialValues.hen_house) : '-'} - {initialValues.hand_house_std !== undefined && - initialValues.hand_house_std > 0 - ? `${initialValues.hand_house_std}%` + {initialValues.hen_house_std !== undefined && + initialValues.hen_house_std > 0 + ? `${initialValues.hen_house_std}%` : '-'} diff --git a/src/config/approval-line.ts b/src/config/approval-line.ts index fad098eb..4914d258 100644 --- a/src/config/approval-line.ts +++ b/src/config/approval-line.ts @@ -74,7 +74,23 @@ export const RECORDING_APPROVAL_LINE: ApprovalLine = [ }, { step_number: 2, - step_name: 'Disetujui', + step_name: 'Approval Head Area', + }, + { + step_number: 3, + step_name: 'Approval Business Unit Vice President', + }, + { + step_number: 4, + step_name: 'Approval Finance', + }, + { + step_number: 5, + step_name: 'Realisasi', + }, + { + step_number: 6, + step_name: 'Selesai', }, ] as const; @@ -130,18 +146,22 @@ export const EXPENSE_REQUEST_APPROVAL_LINE: ApprovalLine = [ }, { step_number: 2, - step_name: 'Approval Manager', + step_name: 'Approval Head Area', }, { step_number: 3, - step_name: 'Approval Finance', + step_name: 'Approval Business Unit Vice President', }, { step_number: 4, - step_name: 'Realisasi', + step_name: 'Approval Finance', }, { step_number: 5, + step_name: 'Realisasi', + }, + { + step_number: 6, step_name: 'Selesai', }, ] as const; diff --git a/src/services/api/expense.ts b/src/services/api/expense.ts index 70e0e339..2a2fb1a7 100644 --- a/src/services/api/expense.ts +++ b/src/services/api/expense.ts @@ -169,13 +169,13 @@ export class ExpenseApiService extends BaseApiService< } } - async approveManager( + async approveHeadArea( id: number, notes?: string ): Promise | undefined> { try { const approveRes = await httpClient>( - `${this.basePath}/approvals/manager`, + `${this.basePath}/approvals/head-area`, { method: 'POST', body: { @@ -196,13 +196,67 @@ export class ExpenseApiService extends BaseApiService< } } - async bulkApproveManager( + async bulkApproveHeadArea( ids: number[], notes?: string ): Promise | undefined> { try { const bulkApproveRes = await httpClient>( - `${this.basePath}/approvals/manager`, + `${this.basePath}/approvals/head-area`, + { + method: 'POST', + body: { + action: 'APPROVED', + approvable_ids: ids, + notes: notes, + }, + } + ); + + return bulkApproveRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + + return undefined; + } + } + + async approveUnitVicePresident( + id: number, + notes?: string + ): Promise | undefined> { + try { + const approveRes = await httpClient>( + `${this.basePath}/approvals/unit-vice-president`, + { + method: 'POST', + body: { + action: 'APPROVED', + approvable_ids: [id], + notes: notes, + }, + } + ); + + return approveRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + + return undefined; + } + } + + async bulkApproveUnitVicePresident( + ids: number[], + notes?: string + ): Promise | undefined> { + try { + const bulkApproveRes = await httpClient>( + `${this.basePath}/approvals/unit-vice-president`, { method: 'POST', body: { @@ -277,13 +331,13 @@ export class ExpenseApiService extends BaseApiService< } } - async rejectManager( + async rejectHeadArea( id: number, notes?: string ): Promise | undefined> { try { const rejectRes = await httpClient>( - `${this.basePath}/approvals/manager`, + `${this.basePath}/approvals/head-area`, { method: 'POST', body: { @@ -304,13 +358,67 @@ export class ExpenseApiService extends BaseApiService< } } - async bulkRejectManager( + async bulkRejectHeadArea( ids: number[], notes?: string ): Promise | undefined> { try { const bulkRejectRes = await httpClient>( - `${this.basePath}/approvals/manager`, + `${this.basePath}/approvals/head-area`, + { + method: 'POST', + body: { + action: 'REJECTED', + approvable_ids: ids, + notes: notes, + }, + } + ); + + return bulkRejectRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + + return undefined; + } + } + + async rejectUnitVicePresident( + id: number, + notes?: string + ): Promise | undefined> { + try { + const rejectRes = await httpClient>( + `${this.basePath}/approvals/unit-vice-president`, + { + method: 'POST', + body: { + action: 'REJECTED', + approvable_ids: [id], + notes: notes, + }, + } + ); + + return rejectRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + + return undefined; + } + } + + async bulkRejectUnitVicePresident( + ids: number[], + notes?: string + ): Promise | undefined> { + try { + const bulkRejectRes = await httpClient>( + `${this.basePath}/approvals/unit-vice-president`, { method: 'POST', body: { diff --git a/src/types/api/production/recording.d.ts b/src/types/api/production/recording.d.ts index 9cf9a625..1728516a 100644 --- a/src/types/api/production/recording.d.ts +++ b/src/types/api/production/recording.d.ts @@ -8,15 +8,15 @@ export type ProductionMetrics = { fcr_value: number; fcr_std?: number; total_chick_qty: number; - hand_day?: number; - hand_house?: number; + hen_day?: number; + hen_house?: number; feed_intake?: number; - egg_mesh?: number; - egg_weight?: number; - hand_day_std?: number; - hand_house_std?: number; feed_intake_std?: number; - egg_mesh_std?: number; + egg_mass?: number; + egg_weight?: number; + hen_day_std?: number; + hen_house_std?: number; + egg_mass_std?: number; egg_weight_std?: number; daily_gain?: number; avg_daily_gain?: number;