refactor(FE-435): Allow realizations without kandang

This commit is contained in:
rstubryan
2025-12-30 19:28:38 +07:00
parent a81a61135f
commit 2bf0f2874e
5 changed files with 219 additions and 187 deletions
@@ -12,7 +12,7 @@ type ExpenseRealizationFormSchemaType = {
label: string;
};
realization_date?: string;
kandangs?: { id: number; name: string }[];
kandangs?: { id?: number; name?: string }[];
supplier?: {
value: number;
label: string;
@@ -20,7 +20,7 @@ type ExpenseRealizationFormSchemaType = {
existing_documents?: { name: string; url: string }[];
documents?: File[];
realizations: {
kandang_id: number;
kandang_id?: number;
cost_items: {
nonstock?: {
value: number;
@@ -49,12 +49,11 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
kandangs: Yup.array()
.of(
Yup.object({
id: Yup.number().required('Kandang wajib dipilih!'),
name: Yup.string().required('Kandang wajib dipilih!'),
id: Yup.number().optional(),
name: Yup.string().optional(),
})
)
.min(1, 'Kandang wajib dipilih!')
.required('Kandang wajib dipilih!'),
.optional(),
supplier: Yup.object({
value: Yup.number().min(1).required(),
@@ -73,7 +72,7 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
realizations: Yup.array()
.of(
Yup.object({
kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').required(),
kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').optional(),
cost_items: Yup.array()
.of(
Yup.object({
@@ -86,12 +85,12 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
notes: Yup.string(),
})
)
.min(1, 'Kandang harus memiliki setidaknya 1 biaya!')
.required('Biaya kandang wajib diisi!'),
.min(1, 'Harus memiliki setidaknya 1 biaya!')
.required('Biaya wajib diisi!'),
})
)
.min(1, 'Biaya kandang wajib diisi!')
.required('Biaya kandang wajib diisi!'),
.min(1, 'Biaya wajib diisi!')
.required('Biaya wajib diisi!'),
});
export const UpdateExpenseRealizationFormSchema = ExpenseRealizationFormSchema;
@@ -150,29 +150,10 @@ const ExpenseRealizationForm = ({
formik.setFieldValue('location', val);
formik.setFieldValue('kandangs', []);
formik.setFieldValue('realizations', []);
};
const kandangsChangeHandler = (
kandangs: { id?: number; name?: string }[]
) => {
formik.setFieldTouched('kandangs', true);
formik.setFieldValue('kandangs', kandangs);
const newRealizations = [...(formik.values.realizations ?? [])];
// add new realizations
kandangs.forEach((kandangItem) => {
if (!kandangItem.id) return;
const isKandangExistInRealization = newRealizations.find(
(realizationItem) => realizationItem.kandang_id === kandangItem.id
);
if (isKandangExistInRealization) return;
newRealizations.push({
kandang_id: kandangItem.id,
// Auto-create realization item for location (without kandang)
formik.setFieldValue('realizations', [
{
cost_items: [
{
nonstock: undefined,
@@ -181,29 +162,57 @@ const ExpenseRealizationForm = ({
notes: '',
},
],
},
]);
};
const kandangsChangeHandler = (
kandangs: { id?: number; name?: string }[]
) => {
formik.setFieldTouched('kandangs', true);
formik.setFieldValue('kandangs', kandangs);
// If no kandangs selected, create realization item for location
if (kandangs.length === 0) {
formik.setFieldValue('realizations', [
{
cost_items: [
{
nonstock: undefined,
quantity: undefined,
price: undefined,
notes: '',
},
],
},
]);
return;
}
// Start with empty array when kandangs are selected
const newRealizations: typeof formik.values.realizations = [];
// add new realizations for each kandang
kandangs.forEach((kandangItem) => {
if (!kandangItem.id) return;
const existingRealization = formik.values.realizations?.find(
(realizationItem) => realizationItem.kandang_id === kandangItem.id
);
newRealizations.push({
kandang_id: kandangItem.id,
cost_items: existingRealization?.cost_items || [
{
nonstock: undefined,
quantity: undefined,
price: undefined,
notes: '',
},
],
});
});
// prune realizations
const kandangIds = new Set(
kandangs
.map((kandang) => kandang.id)
.filter((id): id is number => id !== undefined)
);
const deletedRealizationsIdx: number[] = [];
newRealizations.forEach((realization, idx) => {
const isRealizationValid = kandangIds.has(realization.kandang_id);
if (!isRealizationValid) {
deletedRealizationsIdx.push(idx);
}
});
deletedRealizationsIdx.forEach((deletedRealizationIdx) => {
newRealizations.splice(deletedRealizationIdx, 1);
});
formik.setFieldValue('realizations', newRealizations);
};
@@ -346,7 +355,10 @@ const ExpenseRealizationForm = ({
)}
<ExpenseRealizationKandangDetailExpense
type={type}
formik={formik}
supplierId={formik.values.supplier?.value as number}
location={formik.values.location}
className={{
wrapper: 'col-span-12',
}}
@@ -18,6 +18,11 @@ import { Nonstock } from '@/types/api/master-data/nonstock';
interface ExpenseRealizationKandangDetailExpenseProps {
type?: 'add' | 'edit' | 'detail';
formik: FormikContextType<ExpenseRealizationFormValues>;
supplierId?: number;
location?: {
value: number;
label: string;
};
className?: {
wrapper?: string;
};
@@ -25,12 +30,18 @@ interface ExpenseRealizationKandangDetailExpenseProps {
const ExpenseRealizationKandangDetailExpense: React.FC<
ExpenseRealizationKandangDetailExpenseProps
> = ({ type, formik, className }) => {
> = ({ type, formik, supplierId, location, className }) => {
const {
setInputValue: setNonstockInputValue,
options: nonstockOptions,
isLoadingOptions: isLoadingNonstockOptions,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
} = useSelect<Nonstock>(
NonstockApi.basePath,
'id',
'name',
'search',
supplierId ? { supplier_id: String(supplierId) } : undefined
);
const nonstockChangeHandler = (
kandangExpenseIdx: number,
@@ -82,140 +93,155 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
</div>
<div className='w-full flex flex-col gap-6'>
{formik.values.realizations.length === 0 && (
{!formik.values.supplier?.value && (
<div>
<p className='text-sm text-gray-400 text-center'>
Pilih kandang terlebih dahulu!
Pilih supplier terlebih dahulu!
</p>
</div>
)}
{formik.values.realizations.map((kandangExpense, kandangExpenseIdx) => {
const kandangName = formik.values.kandangs?.find(
(kandang) => kandang.id === kandangExpense.kandang_id
);
{formik.values.realizations.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>
)}
return (
kandangName?.name && (
<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}
</h5>
{formik.values.realizations.length > 0 &&
formik.values.supplier?.value &&
formik.values.realizations.map(
(kandangExpense, kandangExpenseIdx) => {
const kandangName = kandangExpense.kandang_id
? formik.values.kandangs?.find(
(kandang) => kandang.id === kandangExpense.kandang_id
)
: null;
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Nonstock</th>
<th>Total Kuantitas</th>
<th>Harga Satuan</th>
<th>Catatan</th>
</tr>
</thead>
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>
<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
);
}}
options={nonstockOptions}
isLoading={isLoadingNonstockOptions}
onInputChange={setNonstockInputValue}
className={{ wrapper: 'min-w-48' }}
isDisabled
/>
</td>
<td className='p-2'>
<NumberInput
required
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].quantity`}
placeholder='Masukkan Total Kuantitas'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].quantity ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'quantity',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>
<td className='p-2'>
<NumberInput
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].price`}
placeholder='Masukkan Harga Satuan'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].price ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'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={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].notes`}
placeholder='Tuliskan catatan'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].notes ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'notes',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Nonstock</th>
<th>Total Kuantitas</th>
<th>Harga Satuan</th>
<th>Catatan</th>
</tr>
)
)}
</tbody>
</table>
</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
);
}}
options={nonstockOptions}
isLoading={isLoadingNonstockOptions}
onInputChange={setNonstockInputValue}
className={{ wrapper: 'min-w-48' }}
isDisabled
/>
</td>
<td className='p-2'>
<NumberInput
required
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].quantity`}
placeholder='Masukkan Total Kuantitas'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].quantity ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'quantity',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>
<td className='p-2'>
<NumberInput
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].price`}
placeholder='Masukkan Harga Satuan'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].price ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'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={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].notes`}
placeholder='Tuliskan catatan'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].notes ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'notes',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
)
);
})}
)
);
}
)}
</div>
</Card>
);
@@ -22,7 +22,7 @@ type ExpenseFormSchemaType = {
deleted_documents?: number[];
documents?: File[];
expense_nonstocks: {
kandang_id?: number | null;
kandang_id?: number;
cost_items: {
nonstock?: {
value: number;
@@ -79,10 +79,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
expense_nonstocks: Yup.array()
.of(
Yup.object({
kandang_id: Yup.number()
.min(1, 'Wajib memilih kandang!')
.nullable()
.optional(),
kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').optional(),
cost_items: Yup.array()
.of(
Yup.object({
@@ -122,7 +122,7 @@ const ExpenseRequestForm = ({
})),
};
return expenseNonstock.kandang_id !== null
return expenseNonstock.kandang_id
? { ...basePayload, kandang_id: expenseNonstock.kandang_id }
: basePayload;
}),
@@ -151,7 +151,7 @@ const ExpenseRequestForm = ({
})),
};
return expenseNonstock.kandang_id !== null
return expenseNonstock.kandang_id
? { ...basePayload, kandang_id: expenseNonstock.kandang_id }
: basePayload;
}
@@ -199,7 +199,6 @@ const ExpenseRequestForm = ({
// Auto-create expense item for location (without kandang)
formik.setFieldValue('expense_nonstocks', [
{
kandang_id: null,
cost_items: [
{
nonstock: undefined,
@@ -222,7 +221,6 @@ const ExpenseRequestForm = ({
if (kandangs.length === 0) {
formik.setFieldValue('expense_nonstocks', [
{
kandang_id: null,
cost_items: [
{
nonstock: undefined,