@@ -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..cd34e5a3 100644
--- a/src/components/pages/expense/form/ExpenseRequestForm.schema.ts
+++ b/src/components/pages/expense/form/ExpenseRequestForm.schema.ts
@@ -7,18 +7,19 @@ type ExpenseFormSchemaType = {
category?: {
value: 'BOP' | 'NON-BOP';
label: 'BOP' | 'NON-BOP';
- };
+ } | null;
location?: {
value: number;
label: string;
- };
+ } | null;
location_id: number;
transaction_date?: string;
kandangs?: { id?: number; name?: string }[];
supplier?: {
value: number;
label: string;
- };
+ } | null;
+ supplier_id: number;
existing_documents?: { id: number; name: string; url: string }[];
deleted_documents?: number[];
documents?: File[];
@@ -28,7 +29,8 @@ type ExpenseFormSchemaType = {
nonstock?: {
value: number;
label: string;
- };
+ } | null;
+ nonstock_id?: number;
quantity?: number;
price?: number;
notes?: string;
@@ -41,16 +43,24 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema =
category: Yup.object({
value: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
label: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
- }).required('Kategori wajib diisi!'),
+ })
+ .nullable()
+ .optional(),
location: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
- }).required('Lokasi wajib diisi!'),
+ })
+ .nullable()
+ .optional(),
- location_id: Yup.number().min(1).required('Lokasi wajib diisi!'),
+ location_id: Yup.number()
+ .required('Lokasi wajib diisi!')
+ .min(1, 'Lokasi wajib diisi!')
+ .typeError('Lokasi wajib diisi!'),
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
+
kandangs: Yup.array()
.of(
Yup.object({
@@ -63,15 +73,24 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema =
supplier: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
- }).required('Vendor wajib diisi!'),
+ })
+ .nullable()
+ .optional(),
- existing_documents: Yup.array().of(
- Yup.object({
- id: Yup.number().required(),
- name: Yup.string().required(),
- url: Yup.string().required(),
- })
- ),
+ supplier_id: Yup.number()
+ .required('Vendor wajib diisi!')
+ .min(1, 'Vendor wajib diisi!')
+ .typeError('Vendor wajib diisi!'),
+
+ existing_documents: Yup.array()
+ .of(
+ Yup.object({
+ id: Yup.number().required(),
+ name: Yup.string().required(),
+ url: Yup.string().required(),
+ })
+ )
+ .optional(),
deleted_documents: Yup.array().of(Yup.number().required()).optional(),
@@ -87,9 +106,17 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema =
nonstock: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
- }).required('Nonstock wajib diisi!'),
- quantity: Yup.number().required('Total kuantitas wajib diisi!'),
- price: Yup.number().required('Harga satuan wajib diisi!'),
+ }).nullable(),
+ nonstock_id: Yup.number()
+ .required('Nonstock wajib diisi!')
+ .min(1, 'Nonstock wajib diisi!')
+ .typeError('Nonstock wajib diisi!'),
+ quantity: Yup.number()
+ .required('Total kuantitas wajib diisi!')
+ .typeError('Total kuantitas wajib diisi!'),
+ price: Yup.number()
+ .required('Harga satuan wajib diisi!')
+ .typeError('Harga satuan wajib diisi!'),
notes: Yup.string(),
})
)
@@ -104,7 +131,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<
@@ -124,13 +160,13 @@ export const getExpenseFormInitialValues = (
value: initialValues.category,
label: initialValues.category,
}
- : undefined,
+ : null,
location: initialValues?.location
? {
value: initialValues.location.id,
label: initialValues.location.name,
}
- : undefined,
+ : null,
location_id: Number(initialValues?.location.id || 0),
transaction_date: initialValues?.transaction_date
? formatDate(initialValues.transaction_date, 'YYYY-MM-DD')
@@ -144,7 +180,8 @@ export const getExpenseFormInitialValues = (
value: initialValues.supplier.id,
label: initialValues.supplier.name,
}
- : undefined,
+ : null,
+ supplier_id: initialValues?.supplier?.id ?? 0,
existing_documents: initialValues?.documents?.map((doc) => {
const path = doc.path.startsWith('/') ? doc.path.slice(1) : doc.path;
return {
@@ -164,12 +201,25 @@ export const getExpenseFormInitialValues = (
value: expenseItem.nonstock.id,
label: expenseItem.nonstock.name,
},
+ nonstock_id: expenseItem.nonstock.id,
quantity: expenseItem.qty,
price: expenseItem.price,
notes: expenseItem.note,
}))
: [],
}))
- : [],
+ : [
+ {
+ cost_items: [
+ {
+ nonstock: null,
+ nonstock_id: 0,
+ quantity: undefined,
+ price: undefined,
+ notes: '',
+ },
+ ],
+ },
+ ],
};
};
diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx
index 60e55397..a41290b8 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) => {
@@ -201,7 +204,8 @@ const ExpenseRequestForm = ({
{
cost_items: [
{
- nonstock: undefined,
+ nonstock: null,
+ nonstock_id: 0,
quantity: undefined,
price: undefined,
notes: '',
@@ -223,7 +227,8 @@ const ExpenseRequestForm = ({
{
cost_items: [
{
- nonstock: undefined,
+ nonstock: null,
+ nonstock_id: 0,
quantity: undefined,
price: undefined,
notes: '',
@@ -248,7 +253,8 @@ const ExpenseRequestForm = ({
kandang_id: kandangItem.id,
cost_items: existingExpenseNonstock?.cost_items || [
{
- nonstock: undefined,
+ nonstock: null,
+ nonstock_id: 0,
quantity: undefined,
price: undefined,
notes: '',
@@ -263,10 +269,20 @@ const ExpenseRequestForm = ({
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('supplier', true);
formik.setFieldValue('supplier', val);
+
+ const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
+ formik.setFieldValue('supplier_id', supplierId ?? 0);
};
const requestDocumentsChangeHandler = (val: File[]) => {
formik.setFieldTouched('documents', true);
+
+ const invalidFiles = val.filter((file) => file.size > 5 * 1024 * 1024);
+ if (invalidFiles.length > 0) {
+ toast.error('Ukuran dokumen maksimal 5 MB!');
+ return;
+ }
+
formik.setFieldValue('documents', val);
};
@@ -322,6 +338,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 +379,27 @@ const ExpenseRequestForm = ({
|