chore(FE-188,193,199): adjust Expense Request Form and integrate to API

This commit is contained in:
ValdiANS
2025-11-24 09:54:28 +07:00
parent 82eac4a965
commit e4a6b22357
@@ -42,7 +42,6 @@ interface ExpenseFormProps {
initialValues?: Expense;
}
// TODO: integrate this with real API
const ExpenseRequestForm = ({
type = 'add',
initialValues,
@@ -59,7 +58,7 @@ const ExpenseRequestForm = ({
const createExpenseHandler = useCallback(
async (payload: CreateExpensePayload) => {
const createExpenseRes = await ExpenseApi.create(
ExpenseApi.convertPayloadToFormData(payload)
ExpenseApi.convertExpenseRequestPayloadToFormData(payload)
);
if (isResponseError(createExpenseRes)) {
@@ -74,10 +73,15 @@ const ExpenseRequestForm = ({
);
const updateExpenseHandler = useCallback(
async (expenseId: number, payload: UpdateExpensePayload) => {
async (
expenseId: number,
payload: UpdateExpensePayload,
deletedDocumentIds: number[]
) => {
const updateExpenseRes = await ExpenseApi.update(
expenseId,
ExpenseApi.convertPayloadToFormData(payload)
ExpenseApi.convertExpenseRequestUpdatePayloadToFormData(payload),
deletedDocumentIds
);
if (updateExpenseRes?.status === 'error') {
@@ -102,20 +106,17 @@ const ExpenseRequestForm = ({
setExpenseFormErrorMessage('');
const expensePayload: CreateExpensePayload = {
locationId: values.location?.value as number,
kandangIds: values.kandangs
? values.kandangs.map((item) => item.id)
: [],
transaction_date: values.transaction_date as string,
vendorId: values.vendor?.value as number,
request_documents: values.request_documents as File[],
kandang_expenses: values.kandangExpenses.map((kandangExpense) => ({
kandangId: kandangExpense.kandangId,
expenses: kandangExpense.expenses.map((expenseItem) => ({
nonstockId: expenseItem.nonstock?.value as number,
total_quantity: expenseItem.totalQuantity as number,
total_expense: expenseItem.totalExpense as number,
notes: expenseItem.notes,
category: formik.values.category?.value as 'BOP' | 'NON-BOP',
transaction_date: values?.transaction_date as string,
supplier_id: values.supplier?.value as number,
documents: values.documents as File[],
cost_per_kandangs: values.cost_per_kandangs.map((costPerKandang) => ({
kandang_id: costPerKandang.kandang_id,
cost_items: costPerKandang.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number,
total_cost: parseFloat(String(costItem.total_cost)) as number,
notes: costItem.notes ?? '',
})),
})),
};
@@ -126,9 +127,28 @@ const ExpenseRequestForm = ({
break;
case 'edit':
const expenseUpdatePayload: UpdateExpensePayload = {
category: formik.values.category?.value as 'BOP' | 'NON-BOP',
transaction_date: values?.transaction_date as string,
supplier_id: values.supplier?.value as number,
documents: values.documents as File[],
cost_per_kandang: values.cost_per_kandangs.map(
(costPerKandang) => ({
kandang_id: costPerKandang.kandang_id,
cost_items: costPerKandang.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number,
total_cost: parseFloat(String(costItem.total_cost)) as number,
notes: costItem.notes ?? '',
})),
})
),
};
await updateExpenseHandler(
initialValues?.id as number,
expensePayload
expenseUpdatePayload,
formik.values.deleted_documents ?? []
);
break;
}
@@ -145,72 +165,103 @@ const ExpenseRequestForm = ({
const {
setInputValue: setVendorInputValue,
options: vendorOptions,
options: supplierOptions,
isLoadingOptions: isLoadingVendorOptions,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('category', true);
formik.setFieldValue('category', val);
};
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('location', true);
formik.setFieldValue('location', val);
formik.setFieldValue('kandangs', []);
formik.setFieldValue('kandangExpenses', []);
formik.setFieldValue('cost_per_kandangs', []);
};
const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => {
formik.setFieldTouched('kandangs', true);
formik.setFieldValue('kandangs', kandangs);
const newKandangExpenses = [...(formik.values.kandangExpenses ?? [])];
const newCostPerKandangs = [...(formik.values.cost_per_kandangs ?? [])];
// add new kandangExpenses
// add new cost_per_kandangs
kandangs.forEach((kandangItem) => {
const isKandangExistInKandangExpense = newKandangExpenses.find(
(kandangExpenseItem) => kandangExpenseItem.kandangId === kandangItem.id
const isKandangExistInCostPerKandangs = newCostPerKandangs.find(
(costPerKandangItem) => costPerKandangItem.kandang_id === kandangItem.id
);
if (isKandangExistInKandangExpense) return;
if (isKandangExistInCostPerKandangs) return;
newKandangExpenses.push({
kandangId: kandangItem.id,
expenses: [
newCostPerKandangs.push({
kandang_id: kandangItem.id,
cost_items: [
{
nonstock: undefined,
totalExpense: undefined,
totalQuantity: undefined,
quantity: undefined,
total_cost: undefined,
notes: '',
},
],
});
});
// prune kandangExpenses
// prune cost_per_kandangs
const kandangIds = new Set(kandangs.map((kandang) => kandang.id));
const deletedKandangExpensesIdx: number[] = [];
const deletedCostPerKandangsIdx: number[] = [];
newKandangExpenses.forEach((kandangExpense, idx) => {
const isKandangExpenseValid = kandangIds.has(kandangExpense.kandangId);
newCostPerKandangs.forEach((costPerKandang, idx) => {
const isCostPerKandangValid = kandangIds.has(costPerKandang.kandang_id);
if (!isKandangExpenseValid) {
deletedKandangExpensesIdx.push(idx);
if (!isCostPerKandangValid) {
deletedCostPerKandangsIdx.push(idx);
}
});
deletedKandangExpensesIdx.forEach((deletedKandangExpenseIdx) => {
newKandangExpenses.splice(deletedKandangExpenseIdx, 1);
deletedCostPerKandangsIdx.forEach((deletedCostPerKandangIdx) => {
newCostPerKandangs.splice(deletedCostPerKandangIdx, 1);
});
formik.setFieldValue('kandangExpenses', newKandangExpenses);
formik.setFieldValue('cost_per_kandangs', newCostPerKandangs);
};
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('vendor', true);
formik.setFieldValue('vendor', val);
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('supplier', true);
formik.setFieldValue('supplier', val);
};
const requestDocumentsChangeHandler = (val: File[]) => {
formik.setFieldTouched('request_documents', true);
formik.setFieldValue('request_documents', val);
formik.setFieldTouched('documents', true);
formik.setFieldValue('documents', val);
};
const requestDocumentsDeleteHandler = (deletedFileIdx: number) => {
const newRequestDocuments = formik.values.documents;
newRequestDocuments?.splice(deletedFileIdx, 1);
formik.setFieldValue('documents', newRequestDocuments);
};
const deleteDocumentClickHandler = (
deletedDocumentIdx: number,
deletedDocumentId: number
) => {
const newDeletedDocumentIds = [...(formik.values.deleted_documents ?? [])];
const newExistingDocuments = [
...(formik.values.existing_documents ?? []),
].filter((_, idx) => idx !== deletedDocumentIdx);
newDeletedDocumentIds.push(deletedDocumentId);
formik.setFieldTouched('deleted_documents', true);
formik.setFieldValue('deleted_documents', newDeletedDocumentIds);
formik.setFieldTouched('existing_documents', true);
formik.setFieldValue('existing_documents', newExistingDocuments);
};
const deleteExpenseClickHandler = () => {
@@ -269,6 +320,25 @@ const ExpenseRequestForm = ({
className='w-full mt-8 flex flex-col gap-6'
>
<div className='grid grid-cols-12 gap-4'>
<SelectInput
label='Kategori'
required
placeholder='Pilih Kategori'
value={formik.values.category}
onChange={categoryChangeHandler}
options={[
{
value: 'BOP',
label: 'BOP',
},
{
value: 'NON-BOP',
label: 'NON-BOP',
},
]}
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
/>
<SelectInput
label='Lokasi'
required
@@ -278,7 +348,7 @@ const ExpenseRequestForm = ({
options={locationOptions}
isLoading={isLoadingLocationOptions}
onInputChange={setLocationInputValue}
className={{ wrapper: 'col-span-12 sm:col-span-6' }}
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
/>
<DateInput
@@ -288,7 +358,7 @@ const ExpenseRequestForm = ({
value={formik.values.transaction_date}
onChange={formik.handleChange}
className={{
wrapper: 'col-span-12 sm:col-span-6',
wrapper: 'col-span-12 sm:col-span-4',
}}
/>
@@ -306,9 +376,9 @@ const ExpenseRequestForm = ({
label='Vendor'
required
placeholder='Pilih Vendor'
value={formik.values.vendor}
onChange={vendorChangeHandler}
options={vendorOptions}
value={formik.values.supplier}
onChange={supplierChangeHandler}
options={supplierOptions}
isLoading={isLoadingVendorOptions}
onInputChange={setVendorInputValue}
className={{ wrapper: 'col-span-12' }}
@@ -316,9 +386,10 @@ const ExpenseRequestForm = ({
<DropFileInput
label='Dokumen Pengajuan'
name='request_documents'
values={formik.values.request_documents}
name='documents'
values={formik.values.documents}
onChange={requestDocumentsChangeHandler}
onDelete={requestDocumentsDeleteHandler}
accept={{
...ACCEPTED_FILE_TYPE.PDF,
...ACCEPTED_FILE_TYPE.IMAGE,
@@ -336,20 +407,41 @@ const ExpenseRequestForm = ({
{formik.values.existing_documents.map(
(existingDocument, existingDocumentIdx) => (
<li key={existingDocumentIdx}>
<Link
href={existingDocument.url}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{existingDocument.name}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
height={12}
className='inline'
/>
</Link>
<div className='w-full flex flex-wrap justify-between'>
<Link
href={existingDocument.url}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{existingDocument.name}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
height={12}
className='inline'
/>
</Link>
<Button
type='button'
variant='ghost'
color='error'
onClick={() => {
deleteDocumentClickHandler(
existingDocumentIdx,
existingDocument.id
);
}}
className='p-1 rounded-full text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='fluent:delete-12-regular'
width={20}
height={20}
/>
</Button>
</div>
</li>
)
)}
@@ -402,6 +494,17 @@ const ExpenseRequestForm = ({
</div>
)}
{expenseFormErrorMessage && (
<div role='alert' className='alert alert-error w-full'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{expenseFormErrorMessage}</span>
</div>
)}
{type !== 'detail' && (
<div
className={cn('flex flex-row justify-end gap-2', {
@@ -424,17 +527,6 @@ const ExpenseRequestForm = ({
</div>
)}
</div>
{expenseFormErrorMessage && (
<div role='alert' className='alert alert-error'>
<Icon
icon='material-symbols:error-outline'
width={24}
height={24}
/>
<span>{expenseFormErrorMessage}</span>
</div>
)}
</form>
</section>