mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 15:25:46 +00:00
chore(FE-188,193,199): adjust Expense Request Form and integrate to API
This commit is contained in:
@@ -42,7 +42,6 @@ interface ExpenseFormProps {
|
|||||||
initialValues?: Expense;
|
initialValues?: Expense;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: integrate this with real API
|
|
||||||
const ExpenseRequestForm = ({
|
const ExpenseRequestForm = ({
|
||||||
type = 'add',
|
type = 'add',
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -59,7 +58,7 @@ const ExpenseRequestForm = ({
|
|||||||
const createExpenseHandler = useCallback(
|
const createExpenseHandler = useCallback(
|
||||||
async (payload: CreateExpensePayload) => {
|
async (payload: CreateExpensePayload) => {
|
||||||
const createExpenseRes = await ExpenseApi.create(
|
const createExpenseRes = await ExpenseApi.create(
|
||||||
ExpenseApi.convertPayloadToFormData(payload)
|
ExpenseApi.convertExpenseRequestPayloadToFormData(payload)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isResponseError(createExpenseRes)) {
|
if (isResponseError(createExpenseRes)) {
|
||||||
@@ -74,10 +73,15 @@ const ExpenseRequestForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const updateExpenseHandler = useCallback(
|
const updateExpenseHandler = useCallback(
|
||||||
async (expenseId: number, payload: UpdateExpensePayload) => {
|
async (
|
||||||
|
expenseId: number,
|
||||||
|
payload: UpdateExpensePayload,
|
||||||
|
deletedDocumentIds: number[]
|
||||||
|
) => {
|
||||||
const updateExpenseRes = await ExpenseApi.update(
|
const updateExpenseRes = await ExpenseApi.update(
|
||||||
expenseId,
|
expenseId,
|
||||||
ExpenseApi.convertPayloadToFormData(payload)
|
ExpenseApi.convertExpenseRequestUpdatePayloadToFormData(payload),
|
||||||
|
deletedDocumentIds
|
||||||
);
|
);
|
||||||
|
|
||||||
if (updateExpenseRes?.status === 'error') {
|
if (updateExpenseRes?.status === 'error') {
|
||||||
@@ -102,20 +106,17 @@ const ExpenseRequestForm = ({
|
|||||||
setExpenseFormErrorMessage('');
|
setExpenseFormErrorMessage('');
|
||||||
|
|
||||||
const expensePayload: CreateExpensePayload = {
|
const expensePayload: CreateExpensePayload = {
|
||||||
locationId: values.location?.value as number,
|
category: formik.values.category?.value as 'BOP' | 'NON-BOP',
|
||||||
kandangIds: values.kandangs
|
transaction_date: values?.transaction_date as string,
|
||||||
? values.kandangs.map((item) => item.id)
|
supplier_id: values.supplier?.value as number,
|
||||||
: [],
|
documents: values.documents as File[],
|
||||||
transaction_date: values.transaction_date as string,
|
cost_per_kandangs: values.cost_per_kandangs.map((costPerKandang) => ({
|
||||||
vendorId: values.vendor?.value as number,
|
kandang_id: costPerKandang.kandang_id,
|
||||||
request_documents: values.request_documents as File[],
|
cost_items: costPerKandang.cost_items.map((costItem) => ({
|
||||||
kandang_expenses: values.kandangExpenses.map((kandangExpense) => ({
|
nonstock_id: costItem.nonstock?.value as number,
|
||||||
kandangId: kandangExpense.kandangId,
|
quantity: parseFloat(String(costItem.quantity)) as number,
|
||||||
expenses: kandangExpense.expenses.map((expenseItem) => ({
|
total_cost: parseFloat(String(costItem.total_cost)) as number,
|
||||||
nonstockId: expenseItem.nonstock?.value as number,
|
notes: costItem.notes ?? '',
|
||||||
total_quantity: expenseItem.totalQuantity as number,
|
|
||||||
total_expense: expenseItem.totalExpense as number,
|
|
||||||
notes: expenseItem.notes,
|
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
@@ -126,9 +127,28 @@ const ExpenseRequestForm = ({
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'edit':
|
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(
|
await updateExpenseHandler(
|
||||||
initialValues?.id as number,
|
initialValues?.id as number,
|
||||||
expensePayload
|
expenseUpdatePayload,
|
||||||
|
formik.values.deleted_documents ?? []
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -145,72 +165,103 @@ const ExpenseRequestForm = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setVendorInputValue,
|
setInputValue: setVendorInputValue,
|
||||||
options: vendorOptions,
|
options: supplierOptions,
|
||||||
isLoadingOptions: isLoadingVendorOptions,
|
isLoadingOptions: isLoadingVendorOptions,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
} = 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) => {
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched('location', true);
|
formik.setFieldTouched('location', true);
|
||||||
formik.setFieldValue('location', val);
|
formik.setFieldValue('location', val);
|
||||||
|
|
||||||
formik.setFieldValue('kandangs', []);
|
formik.setFieldValue('kandangs', []);
|
||||||
formik.setFieldValue('kandangExpenses', []);
|
formik.setFieldValue('cost_per_kandangs', []);
|
||||||
};
|
};
|
||||||
|
|
||||||
const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => {
|
const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => {
|
||||||
formik.setFieldTouched('kandangs', true);
|
formik.setFieldTouched('kandangs', true);
|
||||||
formik.setFieldValue('kandangs', kandangs);
|
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) => {
|
kandangs.forEach((kandangItem) => {
|
||||||
const isKandangExistInKandangExpense = newKandangExpenses.find(
|
const isKandangExistInCostPerKandangs = newCostPerKandangs.find(
|
||||||
(kandangExpenseItem) => kandangExpenseItem.kandangId === kandangItem.id
|
(costPerKandangItem) => costPerKandangItem.kandang_id === kandangItem.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isKandangExistInKandangExpense) return;
|
if (isKandangExistInCostPerKandangs) return;
|
||||||
|
|
||||||
newKandangExpenses.push({
|
newCostPerKandangs.push({
|
||||||
kandangId: kandangItem.id,
|
kandang_id: kandangItem.id,
|
||||||
expenses: [
|
cost_items: [
|
||||||
{
|
{
|
||||||
nonstock: undefined,
|
nonstock: undefined,
|
||||||
totalExpense: undefined,
|
quantity: undefined,
|
||||||
totalQuantity: undefined,
|
total_cost: undefined,
|
||||||
notes: '',
|
notes: '',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// prune kandangExpenses
|
// prune cost_per_kandangs
|
||||||
const kandangIds = new Set(kandangs.map((kandang) => kandang.id));
|
const kandangIds = new Set(kandangs.map((kandang) => kandang.id));
|
||||||
const deletedKandangExpensesIdx: number[] = [];
|
const deletedCostPerKandangsIdx: number[] = [];
|
||||||
|
|
||||||
newKandangExpenses.forEach((kandangExpense, idx) => {
|
newCostPerKandangs.forEach((costPerKandang, idx) => {
|
||||||
const isKandangExpenseValid = kandangIds.has(kandangExpense.kandangId);
|
const isCostPerKandangValid = kandangIds.has(costPerKandang.kandang_id);
|
||||||
|
|
||||||
if (!isKandangExpenseValid) {
|
if (!isCostPerKandangValid) {
|
||||||
deletedKandangExpensesIdx.push(idx);
|
deletedCostPerKandangsIdx.push(idx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
deletedKandangExpensesIdx.forEach((deletedKandangExpenseIdx) => {
|
deletedCostPerKandangsIdx.forEach((deletedCostPerKandangIdx) => {
|
||||||
newKandangExpenses.splice(deletedKandangExpenseIdx, 1);
|
newCostPerKandangs.splice(deletedCostPerKandangIdx, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
formik.setFieldValue('kandangExpenses', newKandangExpenses);
|
formik.setFieldValue('cost_per_kandangs', newCostPerKandangs);
|
||||||
};
|
};
|
||||||
|
|
||||||
const vendorChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched('vendor', true);
|
formik.setFieldTouched('supplier', true);
|
||||||
formik.setFieldValue('vendor', val);
|
formik.setFieldValue('supplier', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestDocumentsChangeHandler = (val: File[]) => {
|
const requestDocumentsChangeHandler = (val: File[]) => {
|
||||||
formik.setFieldTouched('request_documents', true);
|
formik.setFieldTouched('documents', true);
|
||||||
formik.setFieldValue('request_documents', val);
|
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 = () => {
|
const deleteExpenseClickHandler = () => {
|
||||||
@@ -269,6 +320,25 @@ const ExpenseRequestForm = ({
|
|||||||
className='w-full mt-8 flex flex-col gap-6'
|
className='w-full mt-8 flex flex-col gap-6'
|
||||||
>
|
>
|
||||||
<div className='grid grid-cols-12 gap-4'>
|
<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
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
required
|
required
|
||||||
@@ -278,7 +348,7 @@ const ExpenseRequestForm = ({
|
|||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isLoading={isLoadingLocationOptions}
|
isLoading={isLoadingLocationOptions}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
className={{ wrapper: 'col-span-12 sm:col-span-6' }}
|
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DateInput
|
<DateInput
|
||||||
@@ -288,7 +358,7 @@ const ExpenseRequestForm = ({
|
|||||||
value={formik.values.transaction_date}
|
value={formik.values.transaction_date}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
className={{
|
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'
|
label='Vendor'
|
||||||
required
|
required
|
||||||
placeholder='Pilih Vendor'
|
placeholder='Pilih Vendor'
|
||||||
value={formik.values.vendor}
|
value={formik.values.supplier}
|
||||||
onChange={vendorChangeHandler}
|
onChange={supplierChangeHandler}
|
||||||
options={vendorOptions}
|
options={supplierOptions}
|
||||||
isLoading={isLoadingVendorOptions}
|
isLoading={isLoadingVendorOptions}
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
className={{ wrapper: 'col-span-12' }}
|
className={{ wrapper: 'col-span-12' }}
|
||||||
@@ -316,9 +386,10 @@ const ExpenseRequestForm = ({
|
|||||||
|
|
||||||
<DropFileInput
|
<DropFileInput
|
||||||
label='Dokumen Pengajuan'
|
label='Dokumen Pengajuan'
|
||||||
name='request_documents'
|
name='documents'
|
||||||
values={formik.values.request_documents}
|
values={formik.values.documents}
|
||||||
onChange={requestDocumentsChangeHandler}
|
onChange={requestDocumentsChangeHandler}
|
||||||
|
onDelete={requestDocumentsDeleteHandler}
|
||||||
accept={{
|
accept={{
|
||||||
...ACCEPTED_FILE_TYPE.PDF,
|
...ACCEPTED_FILE_TYPE.PDF,
|
||||||
...ACCEPTED_FILE_TYPE.IMAGE,
|
...ACCEPTED_FILE_TYPE.IMAGE,
|
||||||
@@ -336,20 +407,41 @@ const ExpenseRequestForm = ({
|
|||||||
{formik.values.existing_documents.map(
|
{formik.values.existing_documents.map(
|
||||||
(existingDocument, existingDocumentIdx) => (
|
(existingDocument, existingDocumentIdx) => (
|
||||||
<li key={existingDocumentIdx}>
|
<li key={existingDocumentIdx}>
|
||||||
<Link
|
<div className='w-full flex flex-wrap justify-between'>
|
||||||
href={existingDocument.url}
|
<Link
|
||||||
target='_blank'
|
href={existingDocument.url}
|
||||||
rel='noopener noreferrer'
|
target='_blank'
|
||||||
className='text-blue-500 underline'
|
rel='noopener noreferrer'
|
||||||
>
|
className='text-blue-500 underline'
|
||||||
{existingDocument.name}{' '}
|
>
|
||||||
<Icon
|
{existingDocument.name}{' '}
|
||||||
icon='cuida:open-in-new-tab-outline'
|
<Icon
|
||||||
width={12}
|
icon='cuida:open-in-new-tab-outline'
|
||||||
height={12}
|
width={12}
|
||||||
className='inline'
|
height={12}
|
||||||
/>
|
className='inline'
|
||||||
</Link>
|
/>
|
||||||
|
</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>
|
</li>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -402,6 +494,17 @@ const ExpenseRequestForm = ({
|
|||||||
</div>
|
</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' && (
|
{type !== 'detail' && (
|
||||||
<div
|
<div
|
||||||
className={cn('flex flex-row justify-end gap-2', {
|
className={cn('flex flex-row justify-end gap-2', {
|
||||||
@@ -424,17 +527,6 @@ const ExpenseRequestForm = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user