Files
lti-web-client/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx
T

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;