refactor(FE-435,436): Allow optional kandang and location expenses

This commit is contained in:
rstubryan
2025-12-30 18:53:46 +07:00
parent 8f4f3d93b8
commit d2e88c2061
6 changed files with 107 additions and 61 deletions
@@ -20,10 +20,10 @@ interface ExpenseKandangsTableProps {
locationId?: number; locationId?: number;
type: 'add' | 'edit' | 'detail'; type: 'add' | 'edit' | 'detail';
selectedKandangs: { selectedKandangs: {
id: number; id?: number;
name: string; name?: string;
}[]; }[];
onChange: (kandangs: { id: number; name: string }[]) => void; onChange: (kandangs: { id?: number; name?: string }[]) => void;
className?: { className?: {
wrapper?: string; wrapper?: string;
}; };
@@ -67,7 +67,11 @@ const ExpenseKandangsTable = ({
); );
const [sorting, setSorting] = useState<SortingState>([]); const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>( const [rowSelection, setRowSelection] = useState<Record<string, boolean>>(
convertRowSelectionArrToObj(selectedKandangs.map((item) => item.id)) convertRowSelectionArrToObj(
selectedKandangs
.map((item) => item.id)
.filter((id): id is number => id !== undefined)
)
); );
const kandangsColumns: ColumnDef<Kandang>[] = [ const kandangsColumns: ColumnDef<Kandang>[] = [
@@ -153,7 +153,9 @@ const ExpenseRealizationForm = ({
formik.setFieldValue('realizations', []); formik.setFieldValue('realizations', []);
}; };
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);
@@ -161,6 +163,8 @@ const ExpenseRealizationForm = ({
// add new realizations // add new realizations
kandangs.forEach((kandangItem) => { kandangs.forEach((kandangItem) => {
if (!kandangItem.id) return;
const isKandangExistInRealization = newRealizations.find( const isKandangExistInRealization = newRealizations.find(
(realizationItem) => realizationItem.kandang_id === kandangItem.id (realizationItem) => realizationItem.kandang_id === kandangItem.id
); );
@@ -181,7 +185,11 @@ const ExpenseRealizationForm = ({
}); });
// prune realizations // prune realizations
const kandangIds = new Set(kandangs.map((kandang) => kandang.id)); const kandangIds = new Set(
kandangs
.map((kandang) => kandang.id)
.filter((id): id is number => id !== undefined)
);
const deletedRealizationsIdx: number[] = []; const deletedRealizationsIdx: number[] = [];
newRealizations.forEach((realization, idx) => { newRealizations.forEach((realization, idx) => {
@@ -13,7 +13,7 @@ type ExpenseFormSchemaType = {
}; };
location_id: number; location_id: number;
transaction_date?: string; transaction_date?: string;
kandangs?: { id: number; name: string }[]; kandangs?: { id?: number; name?: string }[];
supplier?: { supplier?: {
value: number; value: number;
label: string; label: string;
@@ -22,7 +22,7 @@ type ExpenseFormSchemaType = {
deleted_documents?: number[]; deleted_documents?: number[];
documents?: File[]; documents?: File[];
expense_nonstocks: { expense_nonstocks: {
kandang_id: number; kandang_id?: number | null;
cost_items: { cost_items: {
nonstock?: { nonstock?: {
value: number; value: number;
@@ -53,12 +53,11 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
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(),
@@ -80,7 +79,10 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
expense_nonstocks: Yup.array() expense_nonstocks: 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!')
.nullable()
.optional(),
cost_items: Yup.array() cost_items: Yup.array()
.of( .of(
Yup.object({ Yup.object({
@@ -113,7 +113,7 @@ const ExpenseRequestForm = ({
supplier_id: values.supplier?.value as number, supplier_id: values.supplier?.value as number,
documents: values.documents as File[], documents: values.documents as File[],
expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => ({ expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => ({
kandang_id: expenseNonstock.kandang_id, kandang_id: expenseNonstock.kandang_id ?? null,
cost_items: expenseNonstock.cost_items.map((costItem) => ({ cost_items: expenseNonstock.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number, nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number, quantity: parseFloat(String(costItem.quantity)) as number,
@@ -137,7 +137,7 @@ const ExpenseRequestForm = ({
documents: values.documents as File[], documents: values.documents as File[],
expense_nonstocks: values.expense_nonstocks.map( expense_nonstocks: values.expense_nonstocks.map(
(expenseNonstock) => ({ (expenseNonstock) => ({
kandang_id: expenseNonstock.kandang_id, kandang_id: expenseNonstock.kandang_id ?? null,
cost_items: expenseNonstock.cost_items.map((costItem) => ({ cost_items: expenseNonstock.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number, nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number, quantity: parseFloat(String(costItem.quantity)) as number,
@@ -185,26 +185,11 @@ const ExpenseRequestForm = ({
formik.setFieldValue('location_id', locationId); formik.setFieldValue('location_id', locationId);
formik.setFieldValue('kandangs', []); formik.setFieldValue('kandangs', []);
formik.setFieldValue('expense_nonstocks', []);
};
const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => { // Auto-create expense item for location (without kandang)
formik.setFieldTouched('kandangs', true); formik.setFieldValue('expense_nonstocks', [
formik.setFieldValue('kandangs', kandangs); {
kandang_id: null,
const newExpenseNonstocks = [...(formik.values.expense_nonstocks ?? [])];
// add new expense_nonstocks
kandangs.forEach((kandangItem) => {
const isKandangExistInExpenseNonstocks = newExpenseNonstocks.find(
(expenseNonstockItem) =>
expenseNonstockItem.kandang_id === kandangItem.id
);
if (isKandangExistInExpenseNonstocks) return;
newExpenseNonstocks.push({
kandang_id: kandangItem.id,
cost_items: [ cost_items: [
{ {
nonstock: undefined, nonstock: undefined,
@@ -213,25 +198,57 @@ const ExpenseRequestForm = ({
notes: '', notes: '',
}, },
], ],
},
]);
};
const kandangsChangeHandler = (
kandangs: { id?: number; name?: string }[]
) => {
formik.setFieldTouched('kandangs', true);
formik.setFieldValue('kandangs', kandangs);
// If no kandangs selected, create expense item for location
if (kandangs.length === 0) {
formik.setFieldValue('expense_nonstocks', [
{
kandang_id: null,
cost_items: [
{
nonstock: undefined,
quantity: undefined,
price: undefined,
notes: '',
},
],
},
]);
return;
}
const newExpenseNonstocks: typeof formik.values.expense_nonstocks = [];
kandangs.forEach((kandangItem) => {
if (!kandangItem.id) return;
const existingExpenseNonstock = formik.values.expense_nonstocks?.find(
(expenseNonstockItem) =>
expenseNonstockItem.kandang_id === kandangItem.id
);
newExpenseNonstocks.push({
kandang_id: kandangItem.id,
cost_items: existingExpenseNonstock?.cost_items || [
{
nonstock: undefined,
quantity: undefined,
price: undefined,
notes: '',
},
],
}); });
}); });
// prune expense_nonstocks
const kandangIds = new Set(kandangs.map((kandang) => kandang.id));
const deletedExpenseNonstocksIdx: number[] = [];
newExpenseNonstocks.forEach((expenseNonstock, idx) => {
const isExpenseNonstockValid = kandangIds.has(expenseNonstock.kandang_id);
if (!isExpenseNonstockValid) {
deletedExpenseNonstocksIdx.push(idx);
}
});
deletedExpenseNonstocksIdx.forEach((deletedExpenseNonstockIdx) => {
newExpenseNonstocks.splice(deletedExpenseNonstockIdx, 1);
});
formik.setFieldValue('expense_nonstocks', newExpenseNonstocks); formik.setFieldValue('expense_nonstocks', newExpenseNonstocks);
}; };
@@ -462,6 +479,7 @@ const ExpenseRequestForm = ({
type={type} type={type}
formik={formik} formik={formik}
supplierId={formik.values.supplier?.value as number} supplierId={formik.values.supplier?.value as number}
location={formik.values.location}
className={{ className={{
wrapper: 'col-span-12', wrapper: 'col-span-12',
}} }}
@@ -22,6 +22,10 @@ interface ExpenseRequestKandangDetailExpenseProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
formik: FormikContextType<ExpenseRequestFormValues>; formik: FormikContextType<ExpenseRequestFormValues>;
supplierId?: number; supplierId?: number;
location?: {
value: number;
label: string;
};
className?: { className?: {
wrapper?: string; wrapper?: string;
}; };
@@ -29,7 +33,7 @@ interface ExpenseRequestKandangDetailExpenseProps {
const ExpenseRequestKandangDetailExpense: React.FC< const ExpenseRequestKandangDetailExpense: React.FC<
ExpenseRequestKandangDetailExpenseProps ExpenseRequestKandangDetailExpenseProps
> = ({ type, formik, supplierId, className }) => { > = ({ type, formik, supplierId, location, className }) => {
const { const {
setInputValue: setNonstockInputValue, setInputValue: setNonstockInputValue,
options: nonstockOptions, options: nonstockOptions,
@@ -120,32 +124,42 @@ const ExpenseRequestKandangDetailExpense: React.FC<
</div> </div>
<div className='w-full flex flex-col gap-6'> <div className='w-full flex flex-col gap-6'>
{(formik.values.expense_nonstocks.length === 0 || {!formik.values.supplier?.value && (
!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.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.expense_nonstocks.length > 0 &&
formik.values.supplier?.value && formik.values.supplier?.value &&
formik.values.expense_nonstocks.map( formik.values.expense_nonstocks.map(
(kandangExpense, kandangExpenseIdx) => { (kandangExpense, kandangExpenseIdx) => {
const kandangName = formik.values.kandangs?.find( const kandangName = kandangExpense.kandang_id
(kandang) => kandang.id === kandangExpense.kandang_id ? formik.values.kandangs?.find(
); (kandang) => kandang.id === kandangExpense.kandang_id
)
: null;
return ( return (
kandangName?.name && ( (kandangName?.name || !kandangExpense.kandang_id) && (
<div <div
key={`kandangExpense-${kandangExpenseIdx}`} key={`kandangExpense-${kandangExpenseIdx}`}
className='w-full flex flex-col gap-4' className='w-full flex flex-col gap-4'
> >
<div> <div>
<h5 className='mb-2 text-lg font-bold text-center'> <h5 className='mb-2 text-lg font-bold text-center'>
Biaya {kandangName?.name} Biaya {kandangName?.name || location?.label || 'Umum'}
</h5> </h5>
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
+2 -2
View File
@@ -62,7 +62,7 @@ export type CreateExpensePayload = {
supplier_id: number; supplier_id: number;
documents: File[]; documents: File[];
expense_nonstocks: { expense_nonstocks: {
kandang_id: number; kandang_id: number | null;
cost_items: { cost_items: {
nonstock_id: number; nonstock_id: number;
quantity: number; quantity: number;
@@ -79,7 +79,7 @@ export type UpdateExpensePayload = {
supplier_id: number; supplier_id: number;
documents: File[]; documents: File[];
expense_nonstocks: { expense_nonstocks: {
kandang_id: number; kandang_id: number | null;
cost_items: { cost_items: {
nonstock_id: number; nonstock_id: number;
quantity: number; quantity: number;