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; label: string;
}; };
realization_date?: string; realization_date?: string;
kandangs?: { id: number; name: string }[]; kandangs?: { id?: number; name?: string }[];
supplier?: { supplier?: {
value: number; value: number;
label: string; label: string;
@@ -20,7 +20,7 @@ type ExpenseRealizationFormSchemaType = {
existing_documents?: { name: string; url: string }[]; existing_documents?: { name: string; url: string }[];
documents?: File[]; documents?: File[];
realizations: { realizations: {
kandang_id: number; kandang_id?: number;
cost_items: { cost_items: {
nonstock?: { nonstock?: {
value: number; value: number;
@@ -49,12 +49,11 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
kandangs: Yup.array() kandangs: Yup.array()
.of( .of(
Yup.object({ Yup.object({
id: Yup.number().required('Kandang wajib dipilih!'), id: Yup.number().optional(),
name: Yup.string().required('Kandang wajib dipilih!'), name: Yup.string().optional(),
}) })
) )
.min(1, 'Kandang wajib dipilih!') .optional(),
.required('Kandang wajib dipilih!'),
supplier: Yup.object({ supplier: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
@@ -73,7 +72,7 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
realizations: Yup.array() realizations: Yup.array()
.of( .of(
Yup.object({ 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() cost_items: Yup.array()
.of( .of(
Yup.object({ Yup.object({
@@ -86,12 +85,12 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
notes: Yup.string(), notes: Yup.string(),
}) })
) )
.min(1, 'Kandang harus memiliki setidaknya 1 biaya!') .min(1, 'Harus memiliki setidaknya 1 biaya!')
.required('Biaya kandang wajib diisi!'), .required('Biaya wajib diisi!'),
}) })
) )
.min(1, 'Biaya kandang wajib diisi!') .min(1, 'Biaya wajib diisi!')
.required('Biaya kandang wajib diisi!'), .required('Biaya wajib diisi!'),
}); });
export const UpdateExpenseRealizationFormSchema = ExpenseRealizationFormSchema; export const UpdateExpenseRealizationFormSchema = ExpenseRealizationFormSchema;
@@ -150,29 +150,10 @@ const ExpenseRealizationForm = ({
formik.setFieldValue('location', val); formik.setFieldValue('location', val);
formik.setFieldValue('kandangs', []); formik.setFieldValue('kandangs', []);
formik.setFieldValue('realizations', []);
};
const kandangsChangeHandler = ( // Auto-create realization item for location (without kandang)
kandangs: { id?: number; name?: string }[] formik.setFieldValue('realizations', [
) => { {
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,
cost_items: [ cost_items: [
{ {
nonstock: undefined, nonstock: undefined,
@@ -181,29 +162,57 @@ const ExpenseRealizationForm = ({
notes: '', 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); formik.setFieldValue('realizations', newRealizations);
}; };
@@ -346,7 +355,10 @@ const ExpenseRealizationForm = ({
)} )}
<ExpenseRealizationKandangDetailExpense <ExpenseRealizationKandangDetailExpense
type={type}
formik={formik} formik={formik}
supplierId={formik.values.supplier?.value as number}
location={formik.values.location}
className={{ className={{
wrapper: 'col-span-12', wrapper: 'col-span-12',
}} }}
@@ -18,6 +18,11 @@ import { Nonstock } from '@/types/api/master-data/nonstock';
interface ExpenseRealizationKandangDetailExpenseProps { interface ExpenseRealizationKandangDetailExpenseProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
formik: FormikContextType<ExpenseRealizationFormValues>; formik: FormikContextType<ExpenseRealizationFormValues>;
supplierId?: number;
location?: {
value: number;
label: string;
};
className?: { className?: {
wrapper?: string; wrapper?: string;
}; };
@@ -25,12 +30,18 @@ interface ExpenseRealizationKandangDetailExpenseProps {
const ExpenseRealizationKandangDetailExpense: React.FC< const ExpenseRealizationKandangDetailExpense: React.FC<
ExpenseRealizationKandangDetailExpenseProps ExpenseRealizationKandangDetailExpenseProps
> = ({ type, formik, className }) => { > = ({ type, formik, supplierId, location, className }) => {
const { const {
setInputValue: setNonstockInputValue, setInputValue: setNonstockInputValue,
options: nonstockOptions, options: nonstockOptions,
isLoadingOptions: isLoadingNonstockOptions, isLoadingOptions: isLoadingNonstockOptions,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name'); } = useSelect<Nonstock>(
NonstockApi.basePath,
'id',
'name',
'search',
supplierId ? { supplier_id: String(supplierId) } : undefined
);
const nonstockChangeHandler = ( const nonstockChangeHandler = (
kandangExpenseIdx: number, kandangExpenseIdx: number,
@@ -82,140 +93,155 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
</div> </div>
<div className='w-full flex flex-col gap-6'> <div className='w-full flex flex-col gap-6'>
{formik.values.realizations.length === 0 && ( {!formik.values.supplier?.value && (
<div> <div>
<p className='text-sm text-gray-400 text-center'> <p className='text-sm text-gray-400 text-center'>
Pilih kandang terlebih dahulu! Pilih supplier terlebih dahulu!
</p> </p>
</div> </div>
)} )}
{formik.values.realizations.map((kandangExpense, kandangExpenseIdx) => { {formik.values.realizations.length === 0 &&
const kandangName = formik.values.kandangs?.find( formik.values.supplier?.value && (
(kandang) => kandang.id === kandangExpense.kandang_id <div>
); <p className='text-sm text-gray-400 text-center'>
Belum ada item biaya. Silakan pilih lokasi terlebih dahulu.
</p>
</div>
)}
return ( {formik.values.realizations.length > 0 &&
kandangName?.name && ( formik.values.supplier?.value &&
<div formik.values.realizations.map(
key={`kandangExpense-${kandangExpenseIdx}`} (kandangExpense, kandangExpenseIdx) => {
className='w-full flex flex-col gap-4' const kandangName = kandangExpense.kandang_id
> ? formik.values.kandangs?.find(
<div> (kandang) => kandang.id === kandangExpense.kandang_id
<h5 className='mb-2 text-lg font-bold text-center'> )
Biaya {kandangName?.name} : null;
</h5>
<div className='overflow-x-auto'> return (
<table className='table'> (kandangName?.name || !kandangExpense.kandang_id) && (
<thead> <div
<tr> key={`kandangExpense-${kandangExpenseIdx}`}
<th>Nonstock</th> className='w-full flex flex-col gap-4'
<th>Total Kuantitas</th> >
<th>Harga Satuan</th> <div>
<th>Catatan</th> <h5 className='mb-2 text-lg font-bold text-center'>
</tr> Biaya {kandangName?.name || location?.label || 'Umum'}
</thead> </h5>
<tbody> <div className='overflow-x-auto'>
{kandangExpense.cost_items.map( <table className='table'>
(expenseItem, expenseIdx) => ( <thead>
<tr key={`expense-${expenseIdx}`}> <tr>
<td className='p-2'> <th>Nonstock</th>
<SelectInput <th>Total Kuantitas</th>
placeholder='Pilih Nonstock' <th>Harga Satuan</th>
value={expenseItem.nonstock} <th>Catatan</th>
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> </tr>
) </thead>
)}
</tbody> <tbody>
</table> {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> );
) }
); )}
})}
</div> </div>
</Card> </Card>
); );
@@ -22,7 +22,7 @@ type ExpenseFormSchemaType = {
deleted_documents?: number[]; deleted_documents?: number[];
documents?: File[]; documents?: File[];
expense_nonstocks: { expense_nonstocks: {
kandang_id?: number | null; kandang_id?: number;
cost_items: { cost_items: {
nonstock?: { nonstock?: {
value: number; value: number;
@@ -79,10 +79,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
expense_nonstocks: Yup.array() expense_nonstocks: Yup.array()
.of( .of(
Yup.object({ Yup.object({
kandang_id: Yup.number() kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').optional(),
.min(1, 'Wajib memilih kandang!')
.nullable()
.optional(),
cost_items: Yup.array() cost_items: Yup.array()
.of( .of(
Yup.object({ Yup.object({
@@ -122,7 +122,7 @@ const ExpenseRequestForm = ({
})), })),
}; };
return expenseNonstock.kandang_id !== null return expenseNonstock.kandang_id
? { ...basePayload, kandang_id: expenseNonstock.kandang_id } ? { ...basePayload, kandang_id: expenseNonstock.kandang_id }
: basePayload; : basePayload;
}), }),
@@ -151,7 +151,7 @@ const ExpenseRequestForm = ({
})), })),
}; };
return expenseNonstock.kandang_id !== null return expenseNonstock.kandang_id
? { ...basePayload, kandang_id: expenseNonstock.kandang_id } ? { ...basePayload, kandang_id: expenseNonstock.kandang_id }
: basePayload; : basePayload;
} }
@@ -199,7 +199,6 @@ const ExpenseRequestForm = ({
// Auto-create expense item for location (without kandang) // Auto-create expense item for location (without kandang)
formik.setFieldValue('expense_nonstocks', [ formik.setFieldValue('expense_nonstocks', [
{ {
kandang_id: null,
cost_items: [ cost_items: [
{ {
nonstock: undefined, nonstock: undefined,
@@ -222,7 +221,6 @@ const ExpenseRequestForm = ({
if (kandangs.length === 0) { if (kandangs.length === 0) {
formik.setFieldValue('expense_nonstocks', [ formik.setFieldValue('expense_nonstocks', [
{ {
kandang_id: null,
cost_items: [ cost_items: [
{ {
nonstock: undefined, nonstock: undefined,