From ec16c6c47ec6eb5ecc4c7b30d6b44907b4683647 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 12 Jan 2026 11:11:11 +0700 Subject: [PATCH] refactor(FE): Add Unit VP approval and rename Manager --- .../pages/expense/ExpenseRequestContent.tsx | 71 +++++++--- .../pages/expense/ExpensesTable.tsx | 81 +++++++++--- src/config/approval-line.ts | 10 +- src/services/api/expense.ts | 124 ++++++++++++++++-- 4 files changed, 239 insertions(+), 47 deletions(-) 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/config/approval-line.ts b/src/config/approval-line.ts index fad098eb..35a730df 100644 --- a/src/config/approval-line.ts +++ b/src/config/approval-line.ts @@ -130,18 +130,22 @@ export const EXPENSE_REQUEST_APPROVAL_LINE: ApprovalLine = [ }, { step_number: 2, - step_name: 'Approval Manager', + step_name: 'Head Area', }, { step_number: 3, - step_name: 'Approval Finance', + step_name: 'Business Unit Vice President', }, { step_number: 4, - step_name: 'Realisasi', + step_name: '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: {