mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
388 lines
15 KiB
TypeScript
388 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import { FormikContextType } from 'formik';
|
|
|
|
import { Icon } from '@iconify/react';
|
|
import Card from '@/components/Card';
|
|
import SelectInput, {
|
|
OptionType,
|
|
useSelect,
|
|
} from '@/components/input/SelectInput';
|
|
import NumberInput from '@/components/input/NumberInput';
|
|
import TextInput from '@/components/input/TextInput';
|
|
import Button from '@/components/Button';
|
|
|
|
import { ExpenseRequestFormValues } from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
|
import { cn } from '@/lib/helper';
|
|
import { NonstockApi } from '@/services/api/master-data';
|
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
|
import { removeArrayItemAndSync } from '@/lib/utils/formik';
|
|
|
|
interface ExpenseRequestKandangDetailExpenseProps {
|
|
type?: 'add' | 'edit' | 'detail';
|
|
formik: FormikContextType<ExpenseRequestFormValues>;
|
|
supplierId?: number;
|
|
location?: {
|
|
value: number;
|
|
label: string;
|
|
} | null;
|
|
className?: {
|
|
wrapper?: string;
|
|
};
|
|
}
|
|
|
|
const ExpenseRequestKandangDetailExpense: React.FC<
|
|
ExpenseRequestKandangDetailExpenseProps
|
|
> = ({ type, formik, supplierId, location, className }) => {
|
|
const {
|
|
setInputValue: setNonstockInputValue,
|
|
options: nonstockOptions,
|
|
isLoadingOptions: isLoadingNonstockOptions,
|
|
} = useSelect<Nonstock>(
|
|
NonstockApi.basePath,
|
|
'id',
|
|
'name',
|
|
'search',
|
|
supplierId ? { supplier_id: String(supplierId) } : undefined
|
|
);
|
|
|
|
const nonstockChangeHandler = (
|
|
kandangExpenseIdx: number,
|
|
expenseIdx: number,
|
|
val: OptionType | OptionType[] | null
|
|
) => {
|
|
formik.setFieldTouched(
|
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
|
true
|
|
);
|
|
formik.setFieldTouched(
|
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`,
|
|
true
|
|
);
|
|
formik.setFieldValue(
|
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
|
val
|
|
);
|
|
|
|
const nonstockId = Array.isArray(val) ? val[0]?.value : val?.value;
|
|
formik.setFieldValue(
|
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`,
|
|
nonstockId ?? 0
|
|
);
|
|
};
|
|
|
|
const addExpenseItemHandler = (kandangExpenseIdx: number) => {
|
|
const newExpensesValue = [
|
|
...formik.values.expense_nonstocks[kandangExpenseIdx].cost_items,
|
|
{
|
|
nonstock: null,
|
|
nonstock_id: 0,
|
|
price: undefined,
|
|
quantity: undefined,
|
|
notes: '',
|
|
},
|
|
];
|
|
|
|
formik.setFieldValue(
|
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items`,
|
|
newExpensesValue
|
|
);
|
|
};
|
|
|
|
const deleteExpenseItemHandler = (
|
|
kandangExpenseIdx: number,
|
|
expenseIdx: number
|
|
) => {
|
|
const path = `expense_nonstocks[${kandangExpenseIdx}].cost_items`;
|
|
|
|
// trims values, errors, and touched at expenseIdx
|
|
removeArrayItemAndSync(formik, path, expenseIdx);
|
|
};
|
|
|
|
const isExpenseRepeaterInputError = (
|
|
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
|
kandangExpenseIdx: number,
|
|
expenseIdx: number
|
|
) => {
|
|
return (
|
|
formik.touched.expense_nonstocks?.[kandangExpenseIdx]?.cost_items?.[
|
|
expenseIdx
|
|
]?.[column] &&
|
|
Boolean(
|
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx] &&
|
|
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx] ===
|
|
'object' &&
|
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
|
expenseIdx
|
|
] &&
|
|
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx]
|
|
.cost_items?.[expenseIdx] === 'object' &&
|
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
|
expenseIdx
|
|
]?.[column]
|
|
)
|
|
);
|
|
};
|
|
|
|
const getExpenseRepeaterErrorMessage = (
|
|
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
|
kandangExpenseIdx: number,
|
|
expenseIdx: number
|
|
): string => {
|
|
const kandangError = formik.errors.expense_nonstocks?.[kandangExpenseIdx];
|
|
|
|
if (!kandangError || typeof kandangError !== 'object') return '';
|
|
|
|
if (!('cost_items' in kandangError)) return '';
|
|
|
|
const costItemsError = kandangError.cost_items?.[expenseIdx];
|
|
|
|
if (!costItemsError || typeof costItemsError !== 'object') return '';
|
|
|
|
const fieldError = costItemsError[column as keyof typeof costItemsError];
|
|
|
|
if (!fieldError) return '';
|
|
|
|
if (typeof fieldError === 'object' && fieldError !== null) {
|
|
return 'Nonstock wajib diisi!';
|
|
}
|
|
|
|
return String(fieldError);
|
|
};
|
|
|
|
return (
|
|
<Card
|
|
className={{
|
|
wrapper: cn('w-full', className?.wrapper),
|
|
body: 'p-4 shadow',
|
|
}}
|
|
>
|
|
<div className='mb-4 text-center'>
|
|
<h4 className='font-bold text-xl'>
|
|
Rincian Pengajuan Biaya Operasional
|
|
</h4>
|
|
</div>
|
|
|
|
<div className='w-full flex flex-col gap-6'>
|
|
{!formik.values.supplier?.value && (
|
|
<div>
|
|
<p className='text-sm text-gray-400 text-center'>
|
|
Pilih supplier terlebih dahulu!
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{formik.values.expense_nonstocks.length === 0 &&
|
|
formik.values.supplier?.value && (
|
|
<div>
|
|
<p className='text-sm text-gray-400 text-center'>
|
|
Belum ada item biaya. Silakan pilih lokasi terlebih dahulu.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{formik.values.expense_nonstocks.length > 0 &&
|
|
formik.values.supplier?.value &&
|
|
formik.values.expense_nonstocks.map(
|
|
(kandangExpense, kandangExpenseIdx) => {
|
|
const kandangName = kandangExpense.kandang_id
|
|
? formik.values.kandangs?.find(
|
|
(kandang) => kandang.id === kandangExpense.kandang_id
|
|
)
|
|
: null;
|
|
|
|
return (
|
|
(kandangName?.name || !kandangExpense.kandang_id) && (
|
|
<div
|
|
key={`kandangExpense-${kandangExpenseIdx}`}
|
|
className='w-full flex flex-col gap-4'
|
|
>
|
|
<div>
|
|
<h5 className='mb-2 text-lg font-bold text-center'>
|
|
Biaya {kandangName?.name || location?.label || 'Umum'}
|
|
</h5>
|
|
|
|
<div className='overflow-x-auto'>
|
|
<table className='table'>
|
|
<thead>
|
|
<tr>
|
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
|
Nonstock
|
|
</th>
|
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
|
Total Kuantitas
|
|
</th>
|
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
|
Harga Satuan
|
|
</th>
|
|
<th>Catatan</th>
|
|
{type !== 'detail' && <th>Aksi</th>}
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
{kandangExpense.cost_items.map(
|
|
(expenseItem, expenseIdx) => (
|
|
<tr key={`expense-${expenseIdx}`}>
|
|
<td className='p-2'>
|
|
<SelectInput
|
|
placeholder='Pilih Nonstock'
|
|
value={expenseItem.nonstock}
|
|
onChange={(val) => {
|
|
nonstockChangeHandler(
|
|
kandangExpenseIdx,
|
|
expenseIdx,
|
|
val
|
|
);
|
|
}}
|
|
isError={isExpenseRepeaterInputError(
|
|
'nonstock_id',
|
|
kandangExpenseIdx,
|
|
expenseIdx
|
|
)}
|
|
errorMessage={getExpenseRepeaterErrorMessage(
|
|
'nonstock_id',
|
|
kandangExpenseIdx,
|
|
expenseIdx
|
|
)}
|
|
options={nonstockOptions}
|
|
isLoading={isLoadingNonstockOptions}
|
|
onInputChange={setNonstockInputValue}
|
|
className={{ wrapper: 'min-w-48' }}
|
|
isClearable={true}
|
|
/>
|
|
</td>
|
|
|
|
<td className='p-2'>
|
|
<NumberInput
|
|
required
|
|
name={`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].quantity`}
|
|
placeholder='Masukkan Total Kuantitas'
|
|
value={
|
|
formik.values.expense_nonstocks[
|
|
kandangExpenseIdx
|
|
].cost_items[expenseIdx].quantity ?? ''
|
|
}
|
|
onChange={formik.handleChange}
|
|
onBlur={formik.handleBlur}
|
|
isError={isExpenseRepeaterInputError(
|
|
'quantity',
|
|
kandangExpenseIdx,
|
|
expenseIdx
|
|
)}
|
|
errorMessage={getExpenseRepeaterErrorMessage(
|
|
'quantity',
|
|
kandangExpenseIdx,
|
|
expenseIdx
|
|
)}
|
|
className={{ wrapper: 'min-w-24' }}
|
|
/>
|
|
</td>
|
|
|
|
<td className='p-2'>
|
|
<NumberInput
|
|
name={`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].price`}
|
|
placeholder='Masukkan Harga Satuan'
|
|
value={
|
|
formik.values.expense_nonstocks[
|
|
kandangExpenseIdx
|
|
].cost_items[expenseIdx].price ?? ''
|
|
}
|
|
onChange={formik.handleChange}
|
|
onBlur={formik.handleBlur}
|
|
isError={isExpenseRepeaterInputError(
|
|
'price',
|
|
kandangExpenseIdx,
|
|
expenseIdx
|
|
)}
|
|
errorMessage={getExpenseRepeaterErrorMessage(
|
|
'price',
|
|
kandangExpenseIdx,
|
|
expenseIdx
|
|
)}
|
|
inputPrefix={
|
|
<span className='text-gray-600 font-medium'>
|
|
Rp
|
|
</span>
|
|
}
|
|
className={{ wrapper: 'min-w-24' }}
|
|
/>
|
|
</td>
|
|
|
|
<td className='p-2'>
|
|
<TextInput
|
|
name={`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].notes`}
|
|
placeholder='Tuliskan catatan'
|
|
value={
|
|
formik.values.expense_nonstocks[
|
|
kandangExpenseIdx
|
|
].cost_items[expenseIdx].notes ?? ''
|
|
}
|
|
onChange={formik.handleChange}
|
|
onBlur={formik.handleBlur}
|
|
isError={isExpenseRepeaterInputError(
|
|
'notes',
|
|
kandangExpenseIdx,
|
|
expenseIdx
|
|
)}
|
|
errorMessage={getExpenseRepeaterErrorMessage(
|
|
'notes',
|
|
kandangExpenseIdx,
|
|
expenseIdx
|
|
)}
|
|
className={{ wrapper: 'min-w-24' }}
|
|
/>
|
|
</td>
|
|
|
|
{type !== 'detail' && (
|
|
<td>
|
|
<Button
|
|
type='button'
|
|
color='error'
|
|
onClick={() =>
|
|
deleteExpenseItemHandler(
|
|
kandangExpenseIdx,
|
|
expenseIdx
|
|
)
|
|
}
|
|
>
|
|
<Icon
|
|
icon='material-symbols:delete-outline-rounded'
|
|
width={24}
|
|
height={24}
|
|
/>
|
|
</Button>
|
|
</td>
|
|
)}
|
|
</tr>
|
|
)
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{type !== 'detail' && (
|
|
<Button
|
|
type='button'
|
|
variant='outline'
|
|
color='success'
|
|
onClick={() => addExpenseItemHandler(kandangExpenseIdx)}
|
|
className='w-fit mx-auto'
|
|
>
|
|
<Icon icon='ic:round-plus' width={24} height={24} />{' '}
|
|
Tambah
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)
|
|
);
|
|
}
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default ExpenseRequestKandangDetailExpense;
|