mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +00:00
Merge branch 'feat/FE/US-391/TASK-435-436-expense-adjustment' into 'development'
[FEAT/FE][US#391/TASK-435-436] Expense Adjustment See merge request mbugroup/lti-web-client!122
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
|||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
|
|
||||||
interface ExpenseRealizationContentProps {
|
interface ExpenseRealizationContentProps {
|
||||||
initialValues?: Expense;
|
initialValues?: Expense;
|
||||||
@@ -103,24 +103,32 @@ const ExpenseRealizationContent = ({
|
|||||||
initialValues?.realization_docs.length > 0 && (
|
initialValues?.realization_docs.length > 0 && (
|
||||||
<ul className='list-disc'>
|
<ul className='list-disc'>
|
||||||
{initialValues?.realization_docs.map(
|
{initialValues?.realization_docs.map(
|
||||||
(realizationDocument, realizationDocumentIdx) => (
|
(realizationDocument, realizationDocumentIdx) => {
|
||||||
<li key={realizationDocumentIdx}>
|
const path = realizationDocument.path.startsWith(
|
||||||
<Link
|
'/'
|
||||||
href={realizationDocument.path}
|
)
|
||||||
target='_blank'
|
? realizationDocument.path.slice(1)
|
||||||
rel='noopener noreferrer'
|
: realizationDocument.path;
|
||||||
className='text-blue-500 underline'
|
const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
|
||||||
>
|
return (
|
||||||
{realizationDocument.path}{' '}
|
<li key={realizationDocumentIdx}>
|
||||||
<Icon
|
<Link
|
||||||
icon='cuida:open-in-new-tab-outline'
|
href={documentUrl}
|
||||||
width={12}
|
target='_blank'
|
||||||
height={12}
|
rel='noopener noreferrer'
|
||||||
className='inline'
|
className='text-blue-500 underline'
|
||||||
/>
|
>
|
||||||
</Link>
|
{realizationDocument.path}{' '}
|
||||||
</li>
|
<Icon
|
||||||
)
|
icon='cuida:open-in-new-tab-outline'
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
className='inline'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
@@ -211,7 +219,7 @@ const ExpenseRealizationContent = ({
|
|||||||
let expenseGrandTotal = 0;
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
kandangExpense.pengajuans?.forEach(
|
kandangExpense.pengajuans?.forEach(
|
||||||
(item) => (expenseGrandTotal += item.price)
|
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -273,7 +281,7 @@ const ExpenseRealizationContent = ({
|
|||||||
let expenseGrandTotal = 0;
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
kandangExpense.realisasi?.forEach(
|
kandangExpense.realisasi?.forEach(
|
||||||
(item) => (expenseGrandTotal += item.price)
|
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
UploadRequestDocumentsFormSchema,
|
UploadRequestDocumentsFormSchema,
|
||||||
UploadRequestDocumentsFormValues,
|
UploadRequestDocumentsFormValues,
|
||||||
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
|
||||||
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
|
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
@@ -408,9 +408,13 @@ const ExpenseRequestContent = ({
|
|||||||
<th>Kandang</th>
|
<th>Kandang</th>
|
||||||
<th>:</th>
|
<th>:</th>
|
||||||
<td>
|
<td>
|
||||||
{initialValues?.kandangs
|
{initialValues?.kandangs &&
|
||||||
.map((item) => item.name)
|
initialValues?.kandangs.some((k) => k.name)
|
||||||
.join(', ')}
|
? initialValues?.kandangs
|
||||||
|
.filter((item) => item.name)
|
||||||
|
.map((item) => item.name)
|
||||||
|
.join(', ')
|
||||||
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -448,7 +452,14 @@ const ExpenseRequestContent = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Nominal Biaya</th>
|
<th>Nominal Biaya</th>
|
||||||
<th>:</th>
|
<th>:</th>
|
||||||
<td>{formatCurrency(initialValues?.grand_total ?? 0)}</td>
|
<td>
|
||||||
|
{formatCurrency(
|
||||||
|
initialValues?.latest_approval.step_number === 4 ||
|
||||||
|
initialValues?.latest_approval.step_number === 5
|
||||||
|
? (initialValues?.total_realisasi ?? 0)
|
||||||
|
: (initialValues?.total_pengajuan ?? 0)
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Status Pencairan</th>
|
<th>Status Pencairan</th>
|
||||||
@@ -482,24 +493,32 @@ const ExpenseRequestContent = ({
|
|||||||
initialValues?.documents.length > 0 && (
|
initialValues?.documents.length > 0 && (
|
||||||
<ul className='list-disc'>
|
<ul className='list-disc'>
|
||||||
{initialValues?.documents.map(
|
{initialValues?.documents.map(
|
||||||
(requestDocument, requestDocumentIdx) => (
|
(requestDocument, requestDocumentIdx) => {
|
||||||
<li key={requestDocumentIdx}>
|
const path = requestDocument.path.startsWith(
|
||||||
<Link
|
'/'
|
||||||
href={requestDocument.path}
|
)
|
||||||
target='_blank'
|
? requestDocument.path.slice(1)
|
||||||
rel='noopener noreferrer'
|
: requestDocument.path;
|
||||||
className='text-blue-500 underline'
|
const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
|
||||||
>
|
return (
|
||||||
{requestDocument.path}{' '}
|
<li key={requestDocumentIdx}>
|
||||||
<Icon
|
<Link
|
||||||
icon='cuida:open-in-new-tab-outline'
|
href={documentUrl}
|
||||||
width={12}
|
target='_blank'
|
||||||
height={12}
|
rel='noopener noreferrer'
|
||||||
className='inline'
|
className='text-blue-500 underline'
|
||||||
/>
|
>
|
||||||
</Link>
|
{requestDocument.path}{' '}
|
||||||
</li>
|
<Icon
|
||||||
)
|
icon='cuida:open-in-new-tab-outline'
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
className='inline'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
@@ -558,7 +577,7 @@ const ExpenseRequestContent = ({
|
|||||||
let expenseGrandTotal = 0;
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
kandangExpense.pengajuans?.forEach(
|
kandangExpense.pengajuans?.forEach(
|
||||||
(item) => (expenseGrandTotal += item.price)
|
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -573,7 +592,9 @@ const ExpenseRequestContent = ({
|
|||||||
colSpan={5}
|
colSpan={5}
|
||||||
className='font-bold text-center text-base-content text-lg'
|
className='font-bold text-center text-base-content text-lg'
|
||||||
>
|
>
|
||||||
Biaya {kandangExpense.name}
|
{kandangExpense.kandang_id && kandangExpense.name
|
||||||
|
? `Biaya ${kandangExpense.name}`
|
||||||
|
: `Biaya ${initialValues?.location.name || 'Umum'}`}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -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>[] = [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatDate } from '@/lib/helper';
|
import { formatDate } from '@/lib/helper';
|
||||||
|
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
|
|
||||||
type ExpenseRealizationFormSchemaType = {
|
type ExpenseRealizationFormSchemaType = {
|
||||||
category?: {
|
category?: {
|
||||||
@@ -12,7 +13,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 +21,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 +50,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 +73,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 +86,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;
|
||||||
@@ -139,10 +139,13 @@ export const getExpenseRealizationFormInitialValues = (
|
|||||||
label: initialValues.supplier.name,
|
label: initialValues.supplier.name,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
existing_documents: initialValues?.realization_docs?.map((doc) => ({
|
existing_documents: initialValues?.realization_docs?.map((doc) => {
|
||||||
name: doc.path,
|
const path = doc.path.startsWith('/') ? doc.path.slice(1) : doc.path;
|
||||||
url: doc.path,
|
return {
|
||||||
})),
|
name: doc.path,
|
||||||
|
url: `${S3_PUBLIC_BASE_URL}/${path}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
documents: [],
|
documents: [],
|
||||||
realizations: initialValues?.kandangs
|
realizations: initialValues?.kandangs
|
||||||
? initialValues.kandangs.map((kandangExpense) => {
|
? initialValues.kandangs.map((kandangExpense) => {
|
||||||
|
|||||||
@@ -150,25 +150,10 @@ const ExpenseRealizationForm = ({
|
|||||||
formik.setFieldValue('location', val);
|
formik.setFieldValue('location', val);
|
||||||
|
|
||||||
formik.setFieldValue('kandangs', []);
|
formik.setFieldValue('kandangs', []);
|
||||||
formik.setFieldValue('realizations', []);
|
|
||||||
};
|
|
||||||
|
|
||||||
const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => {
|
// Auto-create realization item for location (without kandang)
|
||||||
formik.setFieldTouched('kandangs', true);
|
formik.setFieldValue('realizations', [
|
||||||
formik.setFieldValue('kandangs', kandangs);
|
{
|
||||||
|
|
||||||
const newRealizations = [...(formik.values.realizations ?? [])];
|
|
||||||
|
|
||||||
// add new realizations
|
|
||||||
kandangs.forEach((kandangItem) => {
|
|
||||||
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,
|
||||||
@@ -177,25 +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));
|
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -338,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,159 @@ 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>
|
{kandangName?.name
|
||||||
</thead>
|
? `Biaya ${kandangName.name}`
|
||||||
|
: location?.label
|
||||||
|
? `Biaya ${location.label}`
|
||||||
|
: 'Biaya Umum'}
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatDate } from '@/lib/helper';
|
import { formatDate } from '@/lib/helper';
|
||||||
|
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
|
|
||||||
type ExpenseFormSchemaType = {
|
type ExpenseFormSchemaType = {
|
||||||
category?: {
|
category?: {
|
||||||
@@ -11,8 +12,9 @@ type ExpenseFormSchemaType = {
|
|||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
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;
|
||||||
@@ -21,7 +23,7 @@ type ExpenseFormSchemaType = {
|
|||||||
deleted_documents?: number[];
|
deleted_documents?: number[];
|
||||||
documents?: File[];
|
documents?: File[];
|
||||||
expense_nonstocks: {
|
expense_nonstocks: {
|
||||||
kandang_id: number;
|
kandang_id?: number;
|
||||||
cost_items: {
|
cost_items: {
|
||||||
nonstock?: {
|
nonstock?: {
|
||||||
value: number;
|
value: number;
|
||||||
@@ -46,16 +48,17 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
|||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
}).required('Lokasi wajib diisi!'),
|
}).required('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
|
location_id: Yup.number().min(1).required('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
|
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
|
||||||
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(),
|
||||||
@@ -77,7 +80,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().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({
|
||||||
@@ -128,6 +131,7 @@ export const getExpenseFormInitialValues = (
|
|||||||
label: initialValues.location.name,
|
label: initialValues.location.name,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
location_id: Number(initialValues?.location.id || 0),
|
||||||
transaction_date: initialValues?.transaction_date
|
transaction_date: initialValues?.transaction_date
|
||||||
? formatDate(initialValues.transaction_date, 'YYYY-MM-DD')
|
? formatDate(initialValues.transaction_date, 'YYYY-MM-DD')
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -141,11 +145,14 @@ export const getExpenseFormInitialValues = (
|
|||||||
label: initialValues.supplier.name,
|
label: initialValues.supplier.name,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
existing_documents: initialValues?.documents?.map((doc) => ({
|
existing_documents: initialValues?.documents?.map((doc) => {
|
||||||
id: doc.id,
|
const path = doc.path.startsWith('/') ? doc.path.slice(1) : doc.path;
|
||||||
name: doc.path,
|
return {
|
||||||
url: doc.path,
|
id: doc.id,
|
||||||
})),
|
name: doc.path,
|
||||||
|
url: `${S3_PUBLIC_BASE_URL}/${path}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
deleted_documents: [],
|
deleted_documents: [],
|
||||||
documents: [],
|
documents: [],
|
||||||
expense_nonstocks: initialValues?.kandangs
|
expense_nonstocks: initialValues?.kandangs
|
||||||
|
|||||||
@@ -108,18 +108,24 @@ const ExpenseRequestForm = ({
|
|||||||
|
|
||||||
const expensePayload: CreateExpensePayload = {
|
const expensePayload: CreateExpensePayload = {
|
||||||
category: formik.values.category?.value as 'BOP' | 'NON-BOP',
|
category: formik.values.category?.value as 'BOP' | 'NON-BOP',
|
||||||
|
location_id: values.location_id as number,
|
||||||
transaction_date: values?.transaction_date as string,
|
transaction_date: values?.transaction_date as string,
|
||||||
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,
|
const basePayload = {
|
||||||
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,
|
||||||
price: parseFloat(String(costItem.price)) as number,
|
price: parseFloat(String(costItem.price)) as number,
|
||||||
notes: costItem.notes ?? '',
|
notes: costItem.notes ?? '',
|
||||||
})),
|
})),
|
||||||
})),
|
};
|
||||||
|
|
||||||
|
return expenseNonstock.kandang_id
|
||||||
|
? { ...basePayload, kandang_id: expenseNonstock.kandang_id }
|
||||||
|
: basePayload;
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -130,19 +136,25 @@ const ExpenseRequestForm = ({
|
|||||||
case 'edit':
|
case 'edit':
|
||||||
const expenseUpdatePayload: UpdateExpensePayload = {
|
const expenseUpdatePayload: UpdateExpensePayload = {
|
||||||
category: formik.values.category?.value as 'BOP' | 'NON-BOP',
|
category: formik.values.category?.value as 'BOP' | 'NON-BOP',
|
||||||
|
location_id: values.location_id as number,
|
||||||
transaction_date: values?.transaction_date as string,
|
transaction_date: values?.transaction_date as string,
|
||||||
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(
|
expense_nonstocks: values.expense_nonstocks.map(
|
||||||
(expenseNonstock) => ({
|
(expenseNonstock) => {
|
||||||
kandang_id: expenseNonstock.kandang_id,
|
const basePayload = {
|
||||||
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,
|
||||||
price: parseFloat(String(costItem.price)) as number,
|
price: parseFloat(String(costItem.price)) as number,
|
||||||
notes: costItem.notes ?? '',
|
notes: costItem.notes ?? '',
|
||||||
})),
|
})),
|
||||||
})
|
};
|
||||||
|
|
||||||
|
return expenseNonstock.kandang_id
|
||||||
|
? { ...basePayload, kandang_id: expenseNonstock.kandang_id }
|
||||||
|
: basePayload;
|
||||||
|
}
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,27 +191,14 @@ const ExpenseRequestForm = ({
|
|||||||
formik.setFieldTouched('location', true);
|
formik.setFieldTouched('location', true);
|
||||||
formik.setFieldValue('location', val);
|
formik.setFieldValue('location', val);
|
||||||
|
|
||||||
|
const locationId = Array.isArray(val) ? val[0]?.value : val?.value;
|
||||||
|
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);
|
{
|
||||||
|
|
||||||
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,
|
||||||
@@ -208,25 +207,56 @@ 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', [
|
||||||
|
{
|
||||||
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -454,7 +484,10 @@ const ExpenseRequestForm = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<ExpenseRequestKandangDetailExpense
|
<ExpenseRequestKandangDetailExpense
|
||||||
|
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',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ import { removeArrayItemAndSync } from '@/lib/utils/formik';
|
|||||||
interface ExpenseRequestKandangDetailExpenseProps {
|
interface ExpenseRequestKandangDetailExpenseProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
formik: FormikContextType<ExpenseRequestFormValues>;
|
formik: FormikContextType<ExpenseRequestFormValues>;
|
||||||
|
supplierId?: number;
|
||||||
|
location?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
className?: {
|
className?: {
|
||||||
wrapper?: string;
|
wrapper?: string;
|
||||||
};
|
};
|
||||||
@@ -28,12 +33,18 @@ interface ExpenseRequestKandangDetailExpenseProps {
|
|||||||
|
|
||||||
const ExpenseRequestKandangDetailExpense: React.FC<
|
const ExpenseRequestKandangDetailExpense: React.FC<
|
||||||
ExpenseRequestKandangDetailExpenseProps
|
ExpenseRequestKandangDetailExpenseProps
|
||||||
> = ({ 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,
|
||||||
@@ -113,41 +124,57 @@ 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'>
|
||||||
<table className='table'>
|
<table className='table'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nonstock</th>
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
||||||
<th>Total Kuantitas</th>
|
Nonstock
|
||||||
<th>Harga Satuan</th>
|
</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>
|
<th>Catatan</th>
|
||||||
{type !== 'detail' && <th>Aksi</th>}
|
{type !== 'detail' && <th>Aksi</th>}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -219,7 +219,13 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
{ label: 'Lokasi', value: expense?.location.name },
|
{ label: 'Lokasi', value: expense?.location.name },
|
||||||
{
|
{
|
||||||
label: 'Kandang',
|
label: 'Kandang',
|
||||||
value: expense?.kandangs.map((item) => item.name).join(', '),
|
value:
|
||||||
|
expense?.kandangs && expense?.kandangs.some((k) => k.name)
|
||||||
|
? expense?.kandangs
|
||||||
|
.filter((item) => item.name)
|
||||||
|
.map((item) => item.name)
|
||||||
|
.join(', ')
|
||||||
|
: '-',
|
||||||
},
|
},
|
||||||
{ label: 'Vendor', value: expense?.supplier.name },
|
{ label: 'Vendor', value: expense?.supplier.name },
|
||||||
{
|
{
|
||||||
@@ -235,7 +241,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
{ label: 'Nama Pengaju', value: expense?.created_user.name },
|
{ label: 'Nama Pengaju', value: expense?.created_user.name },
|
||||||
{
|
{
|
||||||
label: 'Nominal Biaya',
|
label: 'Nominal Biaya',
|
||||||
value: formatCurrency(expense?.grand_total ?? 0),
|
value: formatCurrency(
|
||||||
|
expense?.latest_approval.step_number === 4 ||
|
||||||
|
expense?.latest_approval.step_number === 5
|
||||||
|
? (expense?.total_realisasi ?? 0)
|
||||||
|
: (expense?.total_pengajuan ?? 0)
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Nominal Pengajuan',
|
label: 'Nominal Pengajuan',
|
||||||
@@ -326,7 +337,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
let expenseRequestTotal = 0;
|
let expenseRequestTotal = 0;
|
||||||
|
|
||||||
kandangExpense.pengajuans?.forEach(
|
kandangExpense.pengajuans?.forEach(
|
||||||
(item) => (expenseRequestTotal += item.price)
|
(item) => (expenseRequestTotal += item.qty * item.price)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -335,7 +346,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
style={ExpensePDFStyle.kandangExpenseContainer}
|
style={ExpensePDFStyle.kandangExpenseContainer}
|
||||||
>
|
>
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
||||||
{kandangExpense.name}
|
{kandangExpense.kandang_id && kandangExpense.name
|
||||||
|
? `Biaya ${kandangExpense.name}`
|
||||||
|
: `Biaya ${expense?.location.name || 'Umum'}`}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
||||||
@@ -484,7 +497,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
let expenseRealizationTotal = 0;
|
let expenseRealizationTotal = 0;
|
||||||
|
|
||||||
kandangExpense.realisasi?.forEach(
|
kandangExpense.realisasi?.forEach(
|
||||||
(item) => (expenseRealizationTotal += item.price)
|
(item) => (expenseRealizationTotal += item.qty * item.price)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -493,7 +506,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
style={ExpensePDFStyle.kandangExpenseContainer}
|
style={ExpensePDFStyle.kandangExpenseContainer}
|
||||||
>
|
>
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
|
||||||
{kandangExpense.name}
|
{kandangExpense.kandang_id && kandangExpense.name
|
||||||
|
? `Biaya ${kandangExpense.name}`
|
||||||
|
: `Biaya ${expense?.location.name || 'Umum'}`}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
<View style={ExpensePDFStyle.kandangExpenseTable}>
|
||||||
|
|||||||
@@ -352,6 +352,9 @@ export const ACCEPTED_FILE_TYPE = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const S3_PUBLIC_BASE_URL = process.env
|
||||||
|
.NEXT_PUBLIC_S3_PUBLIC_BASE_URL as string;
|
||||||
|
|
||||||
export const FILTER_TYPE_OPTIONS = [
|
export const FILTER_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
label: 'Tanggal Realisasi',
|
label: 'Tanggal Realisasi',
|
||||||
|
|||||||
@@ -483,6 +483,7 @@ export class ExpenseApiService extends BaseApiService<
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append('category', payload.category);
|
formData.append('category', payload.category);
|
||||||
|
formData.append('location_id', String(payload.location_id));
|
||||||
formData.append('transaction_date', payload.transaction_date);
|
formData.append('transaction_date', payload.transaction_date);
|
||||||
formData.append('supplier_id', String(payload.supplier_id));
|
formData.append('supplier_id', String(payload.supplier_id));
|
||||||
|
|
||||||
@@ -505,6 +506,7 @@ export class ExpenseApiService extends BaseApiService<
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append('category', payload.category);
|
formData.append('category', payload.category);
|
||||||
|
formData.append('location_id', String(payload.location_id));
|
||||||
formData.append('transaction_date', payload.transaction_date);
|
formData.append('transaction_date', payload.transaction_date);
|
||||||
formData.append('supplier_id', String(payload.supplier_id));
|
formData.append('supplier_id', String(payload.supplier_id));
|
||||||
|
|
||||||
|
|||||||
Vendored
+4
-2
@@ -57,11 +57,12 @@ export type Expense = BaseMetadata & BaseExpense;
|
|||||||
|
|
||||||
export type CreateExpensePayload = {
|
export type CreateExpensePayload = {
|
||||||
category: 'BOP' | 'NON-BOP';
|
category: 'BOP' | 'NON-BOP';
|
||||||
|
location_id: number;
|
||||||
transaction_date: string;
|
transaction_date: string;
|
||||||
supplier_id: number;
|
supplier_id: number;
|
||||||
documents: File[];
|
documents: File[];
|
||||||
expense_nonstocks: {
|
expense_nonstocks: {
|
||||||
kandang_id: number;
|
kandang_id?: number;
|
||||||
cost_items: {
|
cost_items: {
|
||||||
nonstock_id: number;
|
nonstock_id: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
@@ -72,12 +73,13 @@ export type CreateExpensePayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateExpensePayload = {
|
export type UpdateExpensePayload = {
|
||||||
|
location_id: number;
|
||||||
category: 'BOP' | 'NON-BOP';
|
category: 'BOP' | 'NON-BOP';
|
||||||
transaction_date: string;
|
transaction_date: string;
|
||||||
supplier_id: number;
|
supplier_id: number;
|
||||||
documents: File[];
|
documents: File[];
|
||||||
expense_nonstocks: {
|
expense_nonstocks: {
|
||||||
kandang_id: number;
|
kandang_id?: number;
|
||||||
cost_items: {
|
cost_items: {
|
||||||
nonstock_id: number;
|
nonstock_id: number;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user