Merge branch 'dev/hotfix/restu' into 'development'

[FIX/FE] Data Refactor and UI Adjustment (Closing (Penjualan), Transfer Stock, Laporan (Kontrol Pembayaran Customer dan HPP Harian Kandang), Biaya, Recording)

See merge request mbugroup/lti-web-client!210
This commit is contained in:
Rivaldi A N S
2026-01-19 09:15:25 +00:00
23 changed files with 970 additions and 574 deletions
@@ -82,12 +82,12 @@ const SalesReportTable = ({
<div className='font-semibold text-gray-900'>Total Penjualan</div> <div className='font-semibold text-gray-900'>Total Penjualan</div>
), ),
}, },
{ // {
id: 'age', // id: 'age',
accessorKey: 'age', // accessorKey: 'age',
header: 'Umur', // header: 'Umur',
cell: (props) => props.getValue() || '-', // cell: (props) => props.getValue() || '-',
}, // },
{ {
id: 'do_number', id: 'do_number',
accessorKey: 'do_number', accessorKey: 'do_number',
@@ -43,7 +43,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
return ( return (
<> <>
<section className='w-full max-w-7xl pb-16'> <section className='w-full max-w-full pb-16'>
<header className='flex flex-col gap-4'> <header className='flex flex-col gap-4'>
<Button <Button
href='/expense' href='/expense'
@@ -65,7 +65,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
tabs={expenseDetailTabs} tabs={expenseDetailTabs}
variant='lifted' variant='lifted'
className={{ className={{
wrapper: 'max-w-5xl mx-auto mt-4', wrapper: 'mx-auto mt-4',
}} }}
/> />
</section> </section>
@@ -68,7 +68,7 @@ const ExpenseRealizationContent = ({
return ( return (
<div> <div>
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'> <div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'> <div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
<RequirePermission permissions='lti.expense.update.realization'> <RequirePermission permissions='lti.expense.update.realization'>
<Button <Button
@@ -84,7 +84,7 @@ const ExpenseRealizationContent = ({
</div> </div>
</div> </div>
<div className='overflow-x-auto w-full max-w-5xl mx-auto'> <div className='overflow-x-auto w-full mx-auto'>
<table className='table table-sm table-zebra'> <table className='table table-sm table-zebra'>
<tbody> <tbody>
<tr> <tr>
@@ -179,7 +179,7 @@ const ExpenseRealizationContent = ({
</table> </table>
</div> </div>
<div className='w-full max-w-5xl mt-8 mx-auto'> <div className='w-full mt-8 mx-auto'>
<div className='flex flex-row gap-4'> <div className='flex flex-row gap-4'>
<Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}> <Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}>
<div className='w-full flex flex-col gap-2'> <div className='w-full flex flex-col gap-2'>
@@ -216,127 +216,141 @@ const ExpenseRealizationContent = ({
</div> </div>
</div> </div>
<div className='w-full max-w-5xl mt-8 mx-auto'> <div className='w-full mt-8 mx-auto grid grid-cols-2 gap-4'>
<h2 className='font-bold text-xl text-center'> <div>
Rincian Pengajuan Biaya Operasional <h2 className='font-bold text-xl text-center'>
</h2> Rincian Pengajuan Biaya Operasional
</h2>
<div className='w-full mt-2 flex flex-col gap-4'> <div className='w-full mt-2 flex flex-col gap-4'>
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => { {initialValues?.kandangs.map(
let expenseGrandTotal = 0; (kandangExpense, kandangExpenseIdx) => {
let expenseGrandTotal = 0;
kandangExpense.pengajuans?.forEach( kandangExpense.pengajuans?.forEach(
(item) => (expenseGrandTotal += item.qty * item.price) (item) => (expenseGrandTotal += item.qty * item.price)
); );
return ( return (
<div <div
key={kandangExpenseIdx} key={kandangExpenseIdx}
className='overflow-x-auto w-full mx-auto' className='overflow-x-auto w-full mx-auto'
> >
<table className='table table-sm table-zebra'> <table className='table table-sm table-zebra'>
<thead> <thead>
<tr> <tr>
<th <th
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} Biaya {kandangExpense.name}
</th> </th>
</tr>
<tr>
<th>Nonstock</th>
<th>Total Kuantitas</th>
<th>Total Biaya</th>
<th>Catatan</th>
</tr>
</thead>
<tbody>
{kandangExpense.pengajuans?.map(
(pengajuanItem, pengajuanIdx) => (
<tr key={pengajuanIdx}>
<td>{pengajuanItem.nonstock.name}</td>
<td>{pengajuanItem.qty}</td>
<td>{formatCurrency(pengajuanItem.price)}</td>
<td className='w-xs'>{pengajuanItem.note ?? '-'}</td>
</tr> </tr>
) <tr>
)} <th>Nonstock</th>
</tbody> <th>Total Kuantitas</th>
<tfoot> <th>Total Biaya</th>
<tr className='border-y'> <th>Catatan</th>
<th colSpan={2} className='text-right'> </tr>
Total Biaya Keseluruhan: </thead>
</th> <tbody>
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th> {kandangExpense.pengajuans?.map(
</tr> (pengajuanItem, pengajuanIdx) => (
</tfoot> <tr key={pengajuanIdx}>
</table> <td>{pengajuanItem.nonstock.name}</td>
</div> <td>{pengajuanItem.qty}</td>
); <td>{formatCurrency(pengajuanItem.price)}</td>
})} <td className='w-xs'>
{pengajuanItem.note ?? '-'}
</td>
</tr>
)
)}
</tbody>
<tfoot>
<tr className='border-y'>
<th colSpan={2} className='text-right'>
Total Biaya Keseluruhan:
</th>
<th colSpan={2}>
{formatCurrency(expenseGrandTotal)}
</th>
</tr>
</tfoot>
</table>
</div>
);
}
)}
</div>
</div> </div>
</div>
<div className='w-full max-w-5xl mt-8 mx-auto'> <div>
<h2 className='font-bold text-xl text-center'> <h2 className='font-bold text-xl text-center'>
Rincian Realisasi Biaya Operasional Rincian Realisasi Biaya Operasional
</h2> </h2>
<div className='w-full mt-2 flex flex-col gap-4'> <div className='w-full mt-2 flex flex-col gap-4'>
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => { {initialValues?.kandangs.map(
let expenseGrandTotal = 0; (kandangExpense, kandangExpenseIdx) => {
let expenseGrandTotal = 0;
kandangExpense.realisasi?.forEach( kandangExpense.realisasi?.forEach(
(item) => (expenseGrandTotal += item.qty * item.price) (item) => (expenseGrandTotal += item.qty * item.price)
); );
return ( return (
<div <div
key={kandangExpenseIdx} key={kandangExpenseIdx}
className='overflow-x-auto w-full mx-auto' className='overflow-x-auto w-full mx-auto'
> >
<table className='table table-sm table-zebra'> <table className='table table-sm table-zebra'>
<thead> <thead>
<tr> <tr>
<th <th
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} Biaya {kandangExpense.name}
</th> </th>
</tr>
<tr>
<th>Nonstock</th>
<th>Total Kuantitas</th>
<th>Total Biaya</th>
<th>Catatan</th>
</tr>
</thead>
<tbody>
{kandangExpense.realisasi?.map(
(realisasiItem, realisasiIdx) => (
<tr key={realisasiIdx}>
<td>{realisasiItem.nonstock.name}</td>
<td>{realisasiItem.qty}</td>
<td>{formatCurrency(realisasiItem.price)}</td>
<td className='w-xs'>{realisasiItem.note ?? '-'}</td>
</tr> </tr>
) <tr>
)} <th>Nonstock</th>
</tbody> <th>Total Kuantitas</th>
<tfoot> <th>Total Biaya</th>
<tr className='border-y'> <th>Catatan</th>
<th colSpan={2} className='text-right'> </tr>
Total Biaya Keseluruhan: </thead>
</th> <tbody>
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th> {kandangExpense.realisasi?.map(
</tr> (realisasiItem, realisasiIdx) => (
</tfoot> <tr key={realisasiIdx}>
</table> <td>{realisasiItem.nonstock.name}</td>
</div> <td>{realisasiItem.qty}</td>
); <td>{formatCurrency(realisasiItem.price)}</td>
})} <td className='w-xs'>
{realisasiItem.note ?? '-'}
</td>
</tr>
)
)}
</tbody>
<tfoot>
<tr className='border-y'>
<th colSpan={2} className='text-right'>
Total Biaya Keseluruhan:
</th>
<th colSpan={2}>
{formatCurrency(expenseGrandTotal)}
</th>
</tr>
</tfoot>
</table>
</div>
);
}
)}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -273,7 +273,7 @@ const ExpenseRequestContent = ({
<> <>
<div> <div>
{initialValues && !isLoadingApprovalHistory && approvalHistory && ( {initialValues && !isLoadingApprovalHistory && approvalHistory && (
<div className='w-full max-w-5xl my-4 mx-auto'> <div className='w-full my-4 mx-auto'>
<ApprovalSteps approvals={approvalHistory} /> <ApprovalSteps approvals={approvalHistory} />
</div> </div>
)} )}
@@ -281,7 +281,7 @@ const ExpenseRequestContent = ({
<div className='w-full mt-4 flex flex-col gap-4'> <div className='w-full mt-4 flex flex-col gap-4'>
{/* TODO: apply RBAC */} {/* TODO: apply RBAC */}
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'> <div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
{isCurrentApprovalOnHeadArea && ( {isCurrentApprovalOnHeadArea && (
<RequirePermission permissions='lti.expense.approve.head_area'> <RequirePermission permissions='lti.expense.approve.head_area'>
<Button <Button
@@ -414,7 +414,7 @@ const ExpenseRequestContent = ({
</div> </div>
</div> </div>
<div className='overflow-x-auto w-full max-w-5xl mx-auto'> <div className='overflow-x-auto w-full mx-auto'>
<table className='table table-sm table-zebra'> <table className='table table-sm table-zebra'>
<tbody> <tbody>
<tr> <tr>
@@ -608,7 +608,7 @@ const ExpenseRequestContent = ({
</table> </table>
</div> </div>
</div> </div>
<div className='w-full max-w-5xl mt-8 mx-auto'> <div className='w-full mt-8 mx-auto'>
<h2 className='font-bold text-xl text-center'> <h2 className='font-bold text-xl text-center'>
Rincian Pengajuan Biaya Operasional Rincian Pengajuan Biaya Operasional
</h2> </h2>
+20 -28
View File
@@ -54,17 +54,19 @@ const RowOptionsMenu = ({
rejectClickHandler: () => void; rejectClickHandler: () => void;
deleteClickHandler: () => void; deleteClickHandler: () => void;
}) => { }) => {
const showEditButton = const showEditButton = props.row.original.latest_approval
props.row.original.latest_approval.step_number !== 6 && ? props.row.original.latest_approval.step_number !== 6 &&
(props.row.original.latest_approval.step_number === 1 || (props.row.original.latest_approval.step_number === 1 ||
props.row.original.latest_approval.step_number === 2 || props.row.original.latest_approval.step_number === 2 ||
props.row.original.latest_approval.step_number === 3 || props.row.original.latest_approval.step_number === 3 ||
props.row.original.latest_approval.step_number === 4); props.row.original.latest_approval.step_number === 4)
: false;
// TODO: apply RBAC // TODO: apply RBAC
const showRealizationButton = const showRealizationButton = props.row.original.latest_approval
props.row.original.latest_approval.action !== 'REJECTED' && ? props.row.original.latest_approval.action !== 'REJECTED' &&
props.row.original.latest_approval.step_number === 4; props.row.original.latest_approval.step_number === 4
: false;
return ( return (
<RowOptionsMenuWrapper type={type}> <RowOptionsMenuWrapper type={type}>
@@ -278,6 +280,7 @@ const ExpensesTable = () => {
cell: ({ row }) => { cell: ({ row }) => {
const isCheckboxDisabled = const isCheckboxDisabled =
!row.getCanSelect() || !row.getCanSelect() ||
!row.original.latest_approval ||
row.original.latest_approval.action === 'REJECTED'; row.original.latest_approval.action === 'REJECTED';
return ( return (
@@ -413,6 +416,8 @@ const ExpensesTable = () => {
const tableEnableRowSelectionHandler: (row: Row<Expense>) => boolean = ( const tableEnableRowSelectionHandler: (row: Row<Expense>) => boolean = (
row row
) => { ) => {
if (!row.original.latest_approval) return false;
return ( return (
row.original.latest_approval.action !== 'REJECTED' && row.original.latest_approval.action !== 'REJECTED' &&
row.original.latest_approval.step_number !== 6 row.original.latest_approval.step_number !== 6
@@ -692,14 +697,6 @@ const ExpensesTable = () => {
</> </>
)} )}
</div> </div>
<DebouncedTextInput
name='search'
placeholder='Cari Biaya Operasional'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div> </div>
<div className='grid grid-cols-12 justify-end gap-2'> <div className='grid grid-cols-12 justify-end gap-2'>
@@ -753,17 +750,12 @@ const ExpensesTable = () => {
}} }}
/> />
<SelectInput <DebouncedTextInput
label='Baris' name='search'
options={ROWS_OPTIONS} placeholder='Cari Biaya Operasional'
value={{ value={tableFilterState.search}
label: String(tableFilterState.pageSize), onChange={searchChangeHandler}
value: tableFilterState.pageSize, className={{ wrapper: 'col-span-12 max-w-52 justify-self-end' }}
}}
onChange={pageSizeChangeHandler}
className={{
wrapper: 'col-span-12 max-w-28 justify-self-end',
}}
/> />
</div> </div>
</div> </div>
@@ -19,6 +19,7 @@ import { isResponseSuccess } from '@/lib/api-helper';
interface ExpenseKandangsTableProps { interface ExpenseKandangsTableProps {
locationId?: number; locationId?: number;
type: 'add' | 'edit' | 'detail'; type: 'add' | 'edit' | 'detail';
formType?: 'request' | 'realization';
selectedKandangs: { selectedKandangs: {
id?: number; id?: number;
name?: string; name?: string;
@@ -31,6 +32,7 @@ interface ExpenseKandangsTableProps {
const ExpenseKandangsTable = ({ const ExpenseKandangsTable = ({
type, type,
formType = 'request',
locationId, locationId,
selectedKandangs, selectedKandangs,
onChange, onChange,
@@ -173,68 +175,76 @@ const ExpenseKandangsTable = ({
}, [sorting, updateSortingFilter]); }, [sorting, updateSortingFilter]);
return ( return (
<Card <>
className={{ {selectedKandangs.length > 0 && selectedKandangs.some((k) => k.id) && (
wrapper: className?.wrapper, <Card
body: 'p-4 shadow',
}}
>
<Collapse
open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>Pilih Kandang</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
>
<Table<Kandang>
data={isResponseSuccess(kandangs) ? kandangs?.data : []}
columns={kandangsColumns}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0}
totalItems={
isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{ className={{
containerClassName: cn({ wrapper: className?.wrapper,
'mb-20': body: 'p-4 shadow',
isResponseSuccess(kandangs) && kandangs?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 first:flex first:flex-row first:justify-start',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 first:flex first:flex-row first:justify-start',
paginationClassName: cn({
hidden:
isResponseSuccess(kandangs) &&
kandangs?.meta?.total_pages === 1,
}),
}} }}
/> >
</Collapse> <Collapse
</Card> open={open}
onOpenChange={setOpen}
title={
<div className='card-actions p-4 justify-between items-center w-full'>
<div className='card-title'>
{formType === 'realization'
? 'Kandang yang Direalisasikan'
: 'Pilih Kandang'}
</div>
<Icon
icon='material-symbols:keyboard-arrow-down'
width={24}
height={24}
className={cn('text-primary transition-transform', {
'-rotate-180': open,
})}
/>
</div>
}
className='w-full!'
titleClassName='w-full p-0!'
>
<Table<Kandang>
data={isResponseSuccess(kandangs) ? kandangs?.data : []}
columns={kandangsColumns}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0}
totalItems={
isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(kandangs) && kandangs?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 first:flex first:flex-row first:justify-start',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 first:flex first:flex-row first:justify-start',
paginationClassName: cn({
hidden:
isResponseSuccess(kandangs) &&
kandangs?.meta?.total_pages === 1,
}),
}}
/>
</Collapse>
</Card>
)}
</>
); );
}; };
@@ -130,7 +130,7 @@ export const getExpenseRealizationFormInitialValues = (
? formatDate(initialValues?.realization_date, 'YYYY-MM-DD') ? formatDate(initialValues?.realization_date, 'YYYY-MM-DD')
: undefined, : undefined,
kandangs: initialValues?.kandangs.map((kandang) => ({ kandangs: initialValues?.kandangs.map((kandang) => ({
id: kandang.kandang_id, id: kandang.id,
name: kandang.name, name: kandang.name,
})), })),
supplier: initialValues?.supplier supplier: initialValues?.supplier
@@ -249,7 +249,7 @@ const ExpenseRealizationForm = ({
}, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]); }, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]);
return ( return (
<section className='w-full max-w-5xl'> <section className='w-full'>
<header className='flex flex-col gap-4'> <header className='flex flex-col gap-4'>
<Button <Button
href='/expense' href='/expense'
@@ -297,6 +297,7 @@ const ExpenseRealizationForm = ({
<ExpenseKandangsTable <ExpenseKandangsTable
type='detail' type='detail'
formType='realization'
locationId={formik.values.location?.value} locationId={formik.values.location?.value}
selectedKandangs={formik.values.kandangs ?? []} selectedKandangs={formik.values.kandangs ?? []}
onChange={kandangsChangeHandler} onChange={kandangsChangeHandler}
@@ -41,22 +41,25 @@ type ExpenseFormSchemaType = {
export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> = export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
Yup.object({ Yup.object({
category: Yup.object({ category: Yup.object({
value: Yup.string().oneOf(['BOP', 'NON-BOP']).required(), value: Yup.string()
label: Yup.string().oneOf(['BOP', 'NON-BOP']).required(), .oneOf(['BOP', 'NON-BOP'])
.required('Kategori wajib diisi!'),
label: Yup.string()
.oneOf(['BOP', 'NON-BOP'])
.required('Kategori wajib diisi!'),
}) })
.nullable() .nullable()
.optional(), .required('Kategori wajib diisi!')
.typeError('Kategori wajib diisi!'),
location: Yup.object({ location: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}) }).nullable(),
.nullable()
.optional(),
location_id: Yup.number() location_id: Yup.number()
.required('Lokasi wajib diisi!')
.min(1, 'Lokasi wajib diisi!') .min(1, 'Lokasi wajib diisi!')
.required('Lokasi wajib diisi!')
.typeError('Lokasi wajib diisi!'), .typeError('Lokasi wajib diisi!'),
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'), transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
@@ -73,9 +76,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
supplier: Yup.object({ supplier: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required(),
label: Yup.string().required(), label: Yup.string().required(),
}) }).nullable(),
.nullable()
.optional(),
supplier_id: Yup.number() supplier_id: Yup.number()
.required('Vendor wajib diisi!') .required('Vendor wajib diisi!')
@@ -104,9 +105,12 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
.of( .of(
Yup.object({ Yup.object({
nonstock: Yup.object({ nonstock: Yup.object({
value: Yup.number().min(1).required(), value: Yup.number().min(1).required('Nonstock wajib diisi!'),
label: Yup.string().required(), label: Yup.string().required('Nonstock wajib diisi!'),
}).nullable(), })
.nullable()
.required('Nonstock wajib diisi!')
.typeError('Nonstock wajib diisi!'),
nonstock_id: Yup.number() nonstock_id: Yup.number()
.required('Nonstock wajib diisi!') .required('Nonstock wajib diisi!')
.min(1, 'Nonstock wajib diisi!') .min(1, 'Nonstock wajib diisi!')
@@ -190,30 +190,18 @@ const ExpenseRequestForm = ({
formik.setFieldValue('category', val); formik.setFieldValue('category', val);
}; };
const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const locationChangeHandler = useCallback(
formik.setFieldTouched('location', true); (val: OptionType | OptionType[] | null) => {
formik.setFieldValue('location', val); const location = val as OptionType | null;
const locationId = location ? Number(location.value) : 0;
const locationId = Array.isArray(val) ? val[0]?.value : val?.value; formik.setFieldTouched('location', true);
formik.setFieldValue('location_id', locationId); formik.setFieldValue('location', location);
formik.setFieldTouched('location_id', true);
formik.setFieldValue('kandangs', []); formik.setFieldValue('location_id', locationId);
},
// Auto-create expense item for location (without kandang) []
formik.setFieldValue('expense_nonstocks', [ );
{
cost_items: [
{
nonstock: null,
nonstock_id: 0,
quantity: undefined,
price: undefined,
notes: '',
},
],
},
]);
};
const kandangsChangeHandler = ( const kandangsChangeHandler = (
kandangs: { id?: number; name?: string }[] kandangs: { id?: number; name?: string }[]
@@ -268,6 +256,7 @@ const ExpenseRequestForm = ({
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('supplier', true); formik.setFieldTouched('supplier', true);
formik.setFieldTouched('supplier_id', true);
formik.setFieldValue('supplier', val); formik.setFieldValue('supplier', val);
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value; const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
@@ -360,7 +349,7 @@ const ExpenseRequestForm = ({
return ( return (
<> <>
<section className='w-full max-w-5xl'> <section className='w-full'>
<header className='flex flex-col gap-4'> <header className='flex flex-col gap-4'>
<Button <Button
href='/expense' href='/expense'
@@ -407,6 +396,16 @@ const ExpenseRequestForm = ({
placeholder='Pilih Kategori' placeholder='Pilih Kategori'
value={formik.values.category} value={formik.values.category}
onChange={categoryChangeHandler} onChange={categoryChangeHandler}
isError={
formik.touched.category && Boolean(formik.errors.category)
}
errorMessage={
formik.touched.category && formik.errors.category
? typeof formik.errors.category === 'object'
? 'Kategori wajib diisi!'
: (formik.errors.category as string)
: undefined
}
options={[ options={[
{ {
value: 'BOP', value: 'BOP',
@@ -427,8 +426,13 @@ const ExpenseRequestForm = ({
value={formik.values.location} value={formik.values.location}
onChange={locationChangeHandler} onChange={locationChangeHandler}
options={locationOptions} options={locationOptions}
isLoading={isLoadingLocationOptions}
onInputChange={setLocationInputValue} onInputChange={setLocationInputValue}
isLoading={isLoadingLocationOptions}
isError={
formik.touched.location_id && Boolean(formik.errors.location_id)
}
errorMessage={formik.errors.location_id as string}
isClearable
className={{ wrapper: 'col-span-12 sm:col-span-4' }} className={{ wrapper: 'col-span-12 sm:col-span-4' }}
/> />
@@ -438,6 +442,12 @@ const ExpenseRequestForm = ({
required required
value={formik.values.transaction_date} value={formik.values.transaction_date}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={
formik.touched.transaction_date &&
Boolean(formik.errors.transaction_date)
}
errorMessage={formik.errors.transaction_date as string}
className={{ className={{
wrapper: 'col-span-12 sm:col-span-4', wrapper: 'col-span-12 sm:col-span-4',
}} }}
@@ -460,8 +470,12 @@ const ExpenseRequestForm = ({
value={formik.values.supplier} value={formik.values.supplier}
onChange={supplierChangeHandler} onChange={supplierChangeHandler}
options={supplierOptions} options={supplierOptions}
isLoading={isLoadingVendorOptions}
onInputChange={setVendorInputValue} onInputChange={setVendorInputValue}
isLoading={isLoadingVendorOptions}
isError={
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
}
errorMessage={formik.errors.supplier_id as string}
className={{ wrapper: 'col-span-12' }} className={{ wrapper: 'col-span-12' }}
/> />
@@ -55,6 +55,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
true true
); );
formik.setFieldTouched(
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`,
true
);
formik.setFieldValue( formik.setFieldValue(
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
val val
@@ -96,7 +100,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
}; };
const isExpenseRepeaterInputError = ( const isExpenseRepeaterInputError = (
column: 'nonstock' | 'quantity' | 'price' | 'notes', column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
kandangExpenseIdx: number, kandangExpenseIdx: number,
expenseIdx: number expenseIdx: number
) => { ) => {
@@ -105,11 +109,14 @@ const ExpenseRequestKandangDetailExpense: React.FC<
expenseIdx expenseIdx
]?.[column] && ]?.[column] &&
Boolean( Boolean(
formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof formik.errors.expense_nonstocks?.[kandangExpenseIdx] &&
Object && typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx] ===
'object' &&
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[ formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
expenseIdx expenseIdx
] instanceof Object && ] &&
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx]
.cost_items?.[expenseIdx] === 'object' &&
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[ formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
expenseIdx expenseIdx
]?.[column] ]?.[column]
@@ -117,6 +124,32 @@ const ExpenseRequestKandangDetailExpense: React.FC<
); );
}; };
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 ( return (
<Card <Card
className={{ className={{
@@ -202,10 +235,21 @@ const ExpenseRequestKandangDetailExpense: React.FC<
val val
); );
}} }}
isError={isExpenseRepeaterInputError(
'nonstock_id',
kandangExpenseIdx,
expenseIdx
)}
errorMessage={getExpenseRepeaterErrorMessage(
'nonstock_id',
kandangExpenseIdx,
expenseIdx
)}
options={nonstockOptions} options={nonstockOptions}
isLoading={isLoadingNonstockOptions} isLoading={isLoadingNonstockOptions}
onInputChange={setNonstockInputValue} onInputChange={setNonstockInputValue}
className={{ wrapper: 'min-w-48' }} className={{ wrapper: 'min-w-48' }}
isClearable={true}
/> />
</td> </td>
@@ -226,6 +270,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
kandangExpenseIdx, kandangExpenseIdx,
expenseIdx expenseIdx
)} )}
errorMessage={getExpenseRepeaterErrorMessage(
'quantity',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }} className={{ wrapper: 'min-w-24' }}
/> />
</td> </td>
@@ -246,6 +295,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
kandangExpenseIdx, kandangExpenseIdx,
expenseIdx expenseIdx
)} )}
errorMessage={getExpenseRepeaterErrorMessage(
'price',
kandangExpenseIdx,
expenseIdx
)}
inputPrefix={ inputPrefix={
<span className='text-gray-600 font-medium'> <span className='text-gray-600 font-medium'>
Rp Rp
@@ -271,6 +325,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
kandangExpenseIdx, kandangExpenseIdx,
expenseIdx expenseIdx
)} )}
errorMessage={getExpenseRepeaterErrorMessage(
'notes',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }} className={{ wrapper: 'min-w-24' }}
/> />
</td> </td>
@@ -110,6 +110,14 @@ const DeliveryProductObjectSchema = Yup.object({
.typeError('Qty harus berupa angka!'), .typeError('Qty harus berupa angka!'),
}); });
const DeliveryDocumentSchema = Yup.mixed<File | MovementDocument>()
.nullable()
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value): boolean => {
if (!value) return true;
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
return true;
});
const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
delivery_cost: Yup.number() delivery_cost: Yup.number()
.transform((value) => (isNaN(value) || value === 0 ? undefined : value)) .transform((value) => (isNaN(value) || value === 0 ? undefined : value))
@@ -135,13 +143,7 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
}), }),
document_path: Yup.string().nullable().optional(), document_path: Yup.string().nullable().optional(),
document_index: Yup.number().optional(), document_index: Yup.number().optional(),
document: Yup.mixed<File | MovementDocument>() document: DeliveryDocumentSchema,
.nullable()
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
if (!value) return true;
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
return true;
}),
driver_name: Yup.string().required('Nama sopir wajib diisi!'), driver_name: Yup.string().required('Nama sopir wajib diisi!'),
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'), vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
supplier: Yup.object({ supplier: Yup.object({
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -95,7 +95,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
isLoadingOptions: isLoadingWarehouses, isLoadingOptions: isLoadingWarehouses,
loadMore: loadMoreWarehouses, loadMore: loadMoreWarehouses,
rawData: warehouses, rawData: warehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search'); } = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search', {
flag: 'EKSPEDISI',
});
// ===== SELECT INPUT DATA ===== // ===== SELECT INPUT DATA =====
const { const {
@@ -261,6 +263,47 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}, },
}); });
const prevSourceWarehouseIdRef = useRef<number | null>(
formik.values.source_warehouse_id
);
// ===== RESET PRODUCTS WHEN SOURCE WAREHOUSE CHANGES =====
useEffect(() => {
const prevSourceWarehouseId = prevSourceWarehouseIdRef.current;
const currentSourceWarehouseId = formik.values.source_warehouse_id;
if (
prevSourceWarehouseId !== currentSourceWarehouseId &&
prevSourceWarehouseId !== null
) {
formik.setFieldValue('products', [
{
product: null,
product_id: 0,
product_qty: '',
},
]);
formik.setFieldTouched('products', false);
const updatedDeliveries = formik.values.deliveries.map(
(delivery: DeliverySchema) => ({
...delivery,
products: [
{
product: null,
product_id: 0,
product_qty: '',
},
],
})
);
formik.setFieldValue('deliveries', updatedDeliveries);
formik.setFieldTouched('deliveries', false);
}
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) ===== // ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
const { const {
setInputValue: setProductWarehouseSelectInputValue, setInputValue: setProductWarehouseSelectInputValue,
@@ -347,13 +390,71 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}; };
}; };
const handleTransferDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
formik.setFieldValue('transfer_date', e.target.value);
};
// ===== EVENT HANDLERS ===== // ===== EVENT HANDLERS =====
// Product Handlers const handleTransferDateChange = useCallback(
const addProduct = () => { (e: React.ChangeEvent<HTMLInputElement>) => {
formik.setFieldValue('transfer_date', e.target.value);
},
[]
);
const handleSourceWarehouseChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const newSourceWarehouseId = (val as WarehouseOptionType)?.value;
if (
newSourceWarehouseId &&
newSourceWarehouseId === formik.values.destination_warehouse_id
) {
const destinationWarehouseName =
(formik.values.destination_warehouse as WarehouseOptionType)?.label ||
'gudang tujuan';
toast.error(
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
);
return;
}
formik.setFieldTouched('source_warehouse', true);
formik.setFieldValue('source_warehouse', val);
formik.setFieldTouched('source_warehouse_id', true);
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
},
[
formik.values.destination_warehouse_id,
formik.values.destination_warehouse,
]
);
const handleDestinationWarehouseChange = useCallback(
(val: OptionType | OptionType[] | null) => {
const newDestinationWarehouseId = (val as WarehouseOptionType)?.value;
if (
newDestinationWarehouseId &&
newDestinationWarehouseId === formik.values.source_warehouse_id
) {
const sourceWarehouseName =
(formik.values.source_warehouse as WarehouseOptionType)?.label ||
'gudang asal';
toast.error(
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
);
return;
}
formik.setFieldTouched('destination_warehouse', true);
formik.setFieldValue('destination_warehouse', val);
formik.setFieldTouched('destination_warehouse_id', true);
formik.setFieldValue(
'destination_warehouse_id',
newDestinationWarehouseId
);
},
[formik.values.source_warehouse_id, formik.values.source_warehouse]
);
const addProduct = useCallback(() => {
const newProducts = [ const newProducts = [
...(formik.values.products || []), ...(formik.values.products || []),
{ {
@@ -363,22 +464,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}, },
]; ];
formik.setFieldValue('products', newProducts); formik.setFieldValue('products', newProducts);
}; }, []);
const removeProduct = useCallback( const removeProduct = useCallback((i: number) => {
(i: number) => { const updatedProducts =
const updatedProducts = formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
formik.values.products?.reduce((acc: ProductSchema[], item, index) => { if (index !== i) {
if (index !== i) { acc.push(item);
acc.push(item); }
} return acc;
return acc; }, []) ?? [];
}, []) ?? [];
formik.setFieldValue('products', updatedProducts); formik.setFieldValue('products', updatedProducts);
}, }, []);
[formik]
);
const bulkRemoveProduct = useCallback(() => { const bulkRemoveProduct = useCallback(() => {
const updatedProducts = const updatedProducts =
@@ -387,10 +485,45 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) ?? []; ) ?? [];
formik.setFieldValue('products', updatedProducts); formik.setFieldValue('products', updatedProducts);
setSelectedProducts([]); setSelectedProducts([]);
}, [formik, selectedProducts]); }, [formik, selectedProducts, setSelectedProducts]);
// Delivery Handlers const handleProductChange = useCallback(
const addDelivery = () => { (idx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`products.${idx}.product`, true);
formik.setFieldValue(`products.${idx}.product`, val);
formik.setFieldTouched(`products.${idx}.product_id`, true);
formik.setFieldValue(
`products.${idx}.product_id`,
(val as ProductWarehouseOptionType)?.value
);
},
[]
);
const handleProductSelectAllChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
setSelectedProducts(formik.values.products?.map((_, idx) => idx) ?? []);
} else {
setSelectedProducts([]);
}
},
[formik.values.products, setSelectedProducts]
);
const handleProductCheckboxChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const idx = Number(e.target.name.replace('product-', ''));
if (e.target.checked) {
setSelectedProducts((prev) => [...prev, idx]);
} else {
setSelectedProducts((prev) => prev.filter((i) => i !== idx));
}
},
[setSelectedProducts]
);
const addDelivery = useCallback(() => {
formik.setFieldValue('deliveries', [ formik.setFieldValue('deliveries', [
...(formik.values.deliveries || []), ...(formik.values.deliveries || []),
{ {
@@ -410,25 +543,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
], ],
}, },
]); ]);
}; }, []);
const removeDelivery = useCallback( const removeDelivery = useCallback((i: number) => {
(i: number) => { const updatedDeliveries =
const updatedDeliveries = formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => {
formik.values.deliveries?.reduce( if (index !== i) {
(acc: DeliverySchema[], item, index) => { acc.push(item);
if (index !== i) { }
acc.push(item); return acc;
} }, []) ?? [];
return acc;
},
[]
) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries); formik.setFieldValue('deliveries', updatedDeliveries);
}, }, []);
[formik]
);
const bulkRemoveDelivery = useCallback(() => { const bulkRemoveDelivery = useCallback(() => {
const updatedDeliveries = const updatedDeliveries =
@@ -437,33 +564,101 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
) ?? []; ) ?? [];
formik.setFieldValue('deliveries', updatedDeliveries); formik.setFieldValue('deliveries', updatedDeliveries);
setSelectedDeliveries([]); setSelectedDeliveries([]);
}, [formik, selectedDeliveries]); }, [formik, selectedDeliveries, setSelectedDeliveries]);
// Cost Calculation Handlers const handleDeliverySelectAllChange = useCallback(
const handleDeliveryCostChange = useCallback( (e: React.ChangeEvent<HTMLInputElement>) => {
(idx: number, value: number) => { if (e.target.checked) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value); setSelectedDeliveries(
formik.values.deliveries?.map((_, idx) => idx) ?? []
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
const productQty = delivery.products.reduce(
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
); );
if (productQty > 0 && value > 0) { } else {
const perItem = value / productQty; setSelectedDeliveries([]);
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
}
} }
}, },
[formik] [formik.values.deliveries, setSelectedDeliveries]
); );
const handleDeliveryCheckboxChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const idx = Number(e.target.name.replace('delivery-', ''));
if (e.target.checked) {
setSelectedDeliveries((prev) => [...prev, idx]);
} else {
setSelectedDeliveries((prev) => prev.filter((i) => i !== idx));
}
},
[setSelectedDeliveries]
);
const handleDeliveryProductChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(
`deliveries.${deliveryIdx}.products.0.product`,
true
);
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
formik.setFieldTouched(
`deliveries.${deliveryIdx}.products.0.product_id`,
true
);
formik.setFieldValue(
`deliveries.${deliveryIdx}.products.0.product_id`,
(val as OptionType)?.value
);
},
[]
);
const handleDeliverySupplierChange = useCallback(
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
formik.setFieldValue(
`deliveries.${deliveryIdx}.supplier_id`,
(val as OptionType)?.value
);
},
[]
);
const handleDeliveryDocumentChange = useCallback(
(deliveryIdx: number, e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
toast.error('Ukuran dokumen maksimal 5 MB!');
e.target.value = '';
return;
}
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
}
},
[]
);
const handleDeliveryCostChange = useCallback((idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
const delivery = formik.values.deliveries?.[idx];
if (delivery) {
const productQty = delivery.products.reduce(
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
0
);
if (productQty > 0 && value > 0) {
const perItem = value / productQty;
formik.setFieldValue(
`deliveries.${idx}.delivery_cost_per_item`,
perItem
);
} else if (value === 0) {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
}
}
}, []);
const handleDeliveryCostPerItemChange = useCallback( const handleDeliveryCostPerItemChange = useCallback(
(idx: number, value: number) => { (idx: number, value: number) => {
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value); formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
@@ -482,7 +677,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
} }
} }
}, },
[formik] []
); );
const handleDeliveryCostChangeWrapper = useCallback( const handleDeliveryCostChangeWrapper = useCallback(
@@ -957,43 +1152,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang' label='Gudang'
placeholder='Pilih gudang asal...' placeholder='Pilih gudang asal...'
value={formik.values.source_warehouse} value={formik.values.source_warehouse}
onChange={(val) => { onChange={handleSourceWarehouseChange}
const newSourceWarehouseId = (val as WarehouseOptionType)
?.value;
if (newSourceWarehouseId) {
if (
newSourceWarehouseId ===
formik.values.destination_warehouse_id
) {
const destinationWarehouseName =
(
formik.values
.destination_warehouse as WarehouseOptionType
)?.label || 'gudang tujuan';
toast.error(
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
);
return;
}
}
formik.setFieldTouched('source_warehouse', true);
formik.setFieldValue('source_warehouse', val);
formik.setFieldTouched('source_warehouse_id', true);
formik.setFieldValue(
'source_warehouse_id',
newSourceWarehouseId
);
if (
formik.errors.destination_warehouse_id ===
'Gudang tujuan tidak boleh sama dengan gudang asal!'
) {
formik.setFieldError('destination_warehouse_id', undefined);
}
}}
options={warehouseOptions} options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue} onInputChange={setWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreWarehouses} onMenuScrollToBottom={loadMoreWarehouses}
@@ -1057,41 +1216,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
label='Gudang' label='Gudang'
placeholder='Pilih gudang tujuan...' placeholder='Pilih gudang tujuan...'
value={formik.values.destination_warehouse} value={formik.values.destination_warehouse}
onChange={(val) => { onChange={handleDestinationWarehouseChange}
const newDestinationWarehouseId = (val as WarehouseOptionType)
?.value;
if (newDestinationWarehouseId) {
if (
newDestinationWarehouseId ===
formik.values.source_warehouse_id
) {
const sourceWarehouseName =
(formik.values.source_warehouse as WarehouseOptionType)
?.label || 'gudang asal';
toast.error(
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
);
return;
}
}
formik.setFieldTouched('destination_warehouse', true);
formik.setFieldValue('destination_warehouse', val);
formik.setFieldTouched('destination_warehouse_id', true);
formik.setFieldValue(
'destination_warehouse_id',
newDestinationWarehouseId
);
if (
formik.errors.destination_warehouse_id ===
'Gudang tujuan tidak boleh sama dengan gudang asal!'
) {
formik.setFieldError('destination_warehouse_id', undefined);
}
}}
options={warehouseOptions} options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue} onInputChange={setWarehouseSelectInputValue}
isLoading={isLoadingWarehouses} isLoading={isLoadingWarehouses}
@@ -1165,18 +1290,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
selectedProducts.length && selectedProducts.length &&
formik.values.products?.length > 0 formik.values.products?.length > 0
} }
onChange={( onChange={handleProductSelectAllChange}
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedProducts(
formik.values.products?.map((_, idx) => idx) ??
[]
);
} else {
setSelectedProducts([]);
}
}}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -1213,17 +1327,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<CheckboxInput <CheckboxInput
name={`product-${idx}`} name={`product-${idx}`}
checked={selectedProducts.includes(idx)} checked={selectedProducts.includes(idx)}
onChange={( onChange={handleProductCheckboxChange}
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedProducts([...selectedProducts, idx]);
} else {
setSelectedProducts(
selectedProducts.filter((i) => i !== idx)
);
}
}}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -1235,24 +1339,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<SelectInput <SelectInput
required required
value={product.product ?? undefined} value={product.product ?? undefined}
onChange={(val) => { onChange={(val) => handleProductChange(idx, val)}
formik.setFieldTouched(
`products.${idx}.product`,
true
);
formik.setFieldValue(
`products.${idx}.product`,
val
);
formik.setFieldTouched(
`products.${idx}.product_id`,
true
);
formik.setFieldValue(
`products.${idx}.product_id`,
(val as ProductWarehouseOptionType)?.value
);
}}
options={productWarehouseOptions} options={productWarehouseOptions}
onInputChange={setProductWarehouseSelectInputValue} onInputChange={setProductWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreProductWarehouses} onMenuScrollToBottom={loadMoreProductWarehouses}
@@ -1379,19 +1466,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
selectedDeliveries.length && selectedDeliveries.length &&
formik.values.deliveries?.length > 0 formik.values.deliveries?.length > 0
} }
onChange={( onChange={handleDeliverySelectAllChange}
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedDeliveries(
formik.values.deliveries?.map(
(_, idx) => idx
) ?? []
);
} else {
setSelectedDeliveries([]);
}
}}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -1474,20 +1549,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<CheckboxInput <CheckboxInput
name={`delivery-${idx}`} name={`delivery-${idx}`}
checked={selectedDeliveries.includes(idx)} checked={selectedDeliveries.includes(idx)}
onChange={( onChange={handleDeliveryCheckboxChange}
e: React.ChangeEvent<HTMLInputElement>
) => {
if (e.target.checked) {
setSelectedDeliveries([
...selectedDeliveries,
idx,
]);
} else {
setSelectedDeliveries(
selectedDeliveries.filter((i) => i !== idx)
);
}
}}
classNames={{ classNames={{
wrapper: 'flex justify-center', wrapper: 'flex justify-center',
checkbox: 'checkbox checkbox-sm', checkbox: 'checkbox checkbox-sm',
@@ -1500,24 +1562,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required required
placeholder='Pilih produk...' placeholder='Pilih produk...'
value={delivery.products[0]?.product ?? undefined} value={delivery.products[0]?.product ?? undefined}
onChange={(val) => { onChange={(val) =>
formik.setFieldTouched( handleDeliveryProductChange(idx, val)
`deliveries.${idx}.products.0.product`, }
true
);
formik.setFieldValue(
`deliveries.${idx}.products.0.product`,
val
);
formik.setFieldTouched(
`deliveries.${idx}.products.0.product_id`,
true
);
formik.setFieldValue(
`deliveries.${idx}.products.0.product_id`,
(val as OptionType)?.value
);
}}
options={getFilteredProductWarehouseOptions()} options={getFilteredProductWarehouseOptions()}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
isClearable isClearable
@@ -1568,24 +1615,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
required required
placeholder='Pilih supplier...' placeholder='Pilih supplier...'
value={delivery.supplier} value={delivery.supplier}
onChange={(val) => { onChange={(val) =>
formik.setFieldTouched( handleDeliverySupplierChange(idx, val)
`deliveries.${idx}.supplier`, }
true
);
formik.setFieldValue(
`deliveries.${idx}.supplier`,
val
);
formik.setFieldTouched(
`deliveries.${idx}.supplier_id`,
true
);
formik.setFieldValue(
`deliveries.${idx}.supplier_id`,
(val as OptionType)?.value
);
}}
options={supplierOptions} options={supplierOptions}
onInputChange={setSupplierSelectInputValue} onInputChange={setSupplierSelectInputValue}
isLoading={isLoadingSuppliers} isLoading={isLoadingSuppliers}
@@ -1677,20 +1709,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
<FileInput <FileInput
accept='.pdf,.jpg,.jpeg,.png' accept='.pdf,.jpg,.jpeg,.png'
name={`deliveries.${idx}.document`} name={`deliveries.${idx}.document`}
onChange={(e) => { onChange={(e) =>
const file = e.target.files?.[0]; handleDeliveryDocumentChange(idx, e)
if (file) { }
if (file.size > 5 * 1024 * 1024) {
toast.error('Ukuran dokumen maksimal 5 MB!');
e.target.value = '';
return;
}
formik.setFieldValue(
`deliveries.${idx}.document`,
file
);
}
}}
{...isRepeaterInputError( {...isRepeaterInputError(
'deliveries', 'deliveries',
'document', 'document',
@@ -686,10 +686,18 @@ const RecordingTable = () => {
1, 1,
}, },
{ {
header: 'Nama Project', header: 'Lokasi',
cell: (props) => props.row.original.location?.name || '-',
},
{
header: 'Flock',
cell: (props) => cell: (props) =>
props.row.original.project_flock?.flock_name || '-', props.row.original.project_flock?.flock_name || '-',
}, },
{
header: 'Kandang',
cell: (props) => props.row.original.kandang?.name || '-',
},
{ {
header: 'Periode', header: 'Periode',
cell: (props) => props.row.original.project_flock?.period || '-', cell: (props) => props.row.original.project_flock?.period || '-',
@@ -722,12 +730,10 @@ const RecordingTable = () => {
}, },
}, },
{ {
accessorKey: 'warehouse.name',
header: 'Gudang', header: 'Gudang',
cell: (props) => props.row.original.warehouse?.name, cell: (props) => props.row.original.warehouse?.name,
}, },
{ {
accessorKey: 'record_date',
header: 'Waktu Recording', header: 'Waktu Recording',
cell: (props) => cell: (props) =>
formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'), formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'),
@@ -300,7 +300,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<Text>Rata-Rata</Text> <Text>Rata-Rata</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Harga Awal</Text> <Text>Harga/Unit</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>Harga Akhir</Text> <Text>Harga Akhir</Text>
@@ -378,7 +378,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
<Text>{formatNumber(item.average_weight)}</Text> <Text>{formatNumber(item.average_weight)}</Text>
</View> </View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.price)}</Text> <Text>{formatCurrency(item.unit_price)}</Text>
</View> </View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(item.final_price)}</Text> <Text>{formatCurrency(item.final_price)}</Text>
@@ -38,7 +38,7 @@ export const generateCustomerPaymentExcel = (
'Ekor/Qty': formatNumber(item.qty || 0), 'Ekor/Qty': formatNumber(item.qty || 0),
'Berat (Kg)': formatNumber(item.weight || 0), 'Berat (Kg)': formatNumber(item.weight || 0),
AVG: formatNumber(item.average_weight || 0), AVG: formatNumber(item.average_weight || 0),
'Harga Awal': formatCurrency(item.price || 0), 'Harga/Unit': formatCurrency(item.unit_price || 0),
'Harga Akhir': formatCurrency(item.final_price || 0), 'Harga Akhir': formatCurrency(item.final_price || 0),
Total: formatCurrency(item.total_price || 0), Total: formatCurrency(item.total_price || 0),
Pembayaran: formatCurrency(item.payment_amount || 0), Pembayaran: formatCurrency(item.payment_amount || 0),
@@ -62,7 +62,7 @@ export const generateCustomerPaymentExcel = (
'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0), 'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0),
'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0), 'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0),
AVG: '', AVG: '',
'Harga Awal': '', 'Harga/Unit': '',
'Harga Akhir': formatCurrency( 'Harga Akhir': formatCurrency(
customerReport.summary.total_final_amount || 0 customerReport.summary.total_final_amount || 0
), ),
@@ -89,7 +89,7 @@ export const generateCustomerPaymentExcel = (
{ wch: 10 }, // Ekor/Qty { wch: 10 }, // Ekor/Qty
{ wch: 12 }, // Berat { wch: 12 }, // Berat
{ wch: 10 }, // AVG { wch: 10 }, // AVG
{ wch: 15 }, // Harga Awal { wch: 15 }, // Harga/Unit
{ wch: 15 }, // Harga Akhir { wch: 15 }, // Harga Akhir
{ wch: 15 }, // Total { wch: 15 }, // Total
{ wch: 15 }, // Pembayaran { wch: 15 }, // Pembayaran
@@ -106,7 +106,11 @@ const CustomerPaymentTab = () => {
}; };
const getPaymentStatusText = (notes: string) => { const getPaymentStatusText = (notes: string) => {
return notes; return notes
.toLowerCase()
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}; };
// ===== FILTER HANDLERS ===== // ===== FILTER HANDLERS =====
@@ -159,7 +163,7 @@ const CustomerPaymentTab = () => {
isSubmitted isSubmitted
? () => { ? () => {
const params = { const params = {
customer_id: customer_ids:
filterCustomer.length > 0 filterCustomer.length > 0
? filterCustomer.map((v) => String(v.value)).join(',') ? filterCustomer.map((v) => String(v.value)).join(',')
: undefined, : undefined,
@@ -180,7 +184,7 @@ const CustomerPaymentTab = () => {
: null, : null,
([, params]) => ([, params]) =>
FinanceApi.getCustomerPaymentReport( FinanceApi.getCustomerPaymentReport(
params.customer_id, params.customer_ids,
undefined, // TODO: Change to params.sales_id when BE is ready undefined, // TODO: Change to params.sales_id when BE is ready
undefined, // TODO: Change to params.filter_by when BE is ready undefined, // TODO: Change to params.filter_by when BE is ready
params.start_date, params.start_date,
@@ -203,7 +207,7 @@ const CustomerPaymentTab = () => {
CustomerPaymentReport[] | null CustomerPaymentReport[] | null
> => { > => {
const params = { const params = {
customer_id: customer_ids:
filterCustomer.length > 0 filterCustomer.length > 0
? filterCustomer.map((v) => String(v.value)).join(',') ? filterCustomer.map((v) => String(v.value)).join(',')
: undefined, : undefined,
@@ -219,7 +223,7 @@ const CustomerPaymentTab = () => {
}; };
const response = await FinanceApi.getCustomerPaymentReport( const response = await FinanceApi.getCustomerPaymentReport(
params.customer_id, params.customer_ids,
undefined, // TODO: Change to params.sales_id when BE is ready undefined, // TODO: Change to params.sales_id when BE is ready
undefined, // TODO: Change to params.filter_by when BE is ready undefined, // TODO: Change to params.filter_by when BE is ready
params.start_date, params.start_date,
@@ -336,7 +340,9 @@ const CustomerPaymentTab = () => {
const value = props.row.original.aging_day; const value = props.row.original.aging_day;
return ( return (
<div className='text-center'> <div className='text-center'>
{value && value > 0 ? `${formatNumber(value)} hari` : '-'} {value !== null && value !== undefined
? `${formatNumber(value)} hari`
: '-'}
</div> </div>
); );
}, },
@@ -405,12 +411,12 @@ const CustomerPaymentTab = () => {
), ),
}, },
{ {
id: 'price', id: 'unit_price',
header: 'Harga Awal', header: 'Harga/Unit',
accessorKey: 'price', accessorKey: 'unit_price',
enableSorting: false, enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.price; const value = props.row.original.unit_price;
return <div className='text-right'>{formatCurrency(value)}</div>; return <div className='text-right'>{formatCurrency(value)}</div>;
}, },
footer: () => ( footer: () => (
@@ -510,7 +516,7 @@ const CustomerPaymentTab = () => {
status: getPaymentStatusIndicatorColor(value), status: getPaymentStatusIndicatorColor(value),
}} }}
> >
<span>{getPaymentStatusText(value)}</span> <span className='capitalize'>{getPaymentStatusText(value)}</span>
</Badge> </Badge>
); );
}, },
@@ -246,7 +246,12 @@ const createPDFDocument = (
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
<Text>HPP Telur (RP/KG)</Text> <Text>HPP Telur (RP/KG)</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}> <View
style={[
pdfStyles.tableCellHeaderRight,
{ flex: 1.2, borderRightWidth: 0 },
]}
>
<Text>Nominal Sisa</Text> <Text>Nominal Sisa</Text>
</View> </View>
</View> </View>
@@ -301,7 +306,12 @@ const createPDFDocument = (
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}> <View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
<Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text> <Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text>
</View> </View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}> <View
style={[
pdfStyles.tableCellRight,
{ flex: 1.2, borderRightWidth: 0 },
]}
>
<Text>{formatCurrency(group.egg_value_rp)}</Text> <Text>{formatCurrency(group.egg_value_rp)}</Text>
</View> </View>
</View> </View>
@@ -347,7 +357,12 @@ const createPDFDocument = (
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
<Text>HPP Telur (RP/KG)</Text> <Text>HPP Telur (RP/KG)</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}> <View
style={[
pdfStyles.tableCellHeaderRight,
{ flex: 1.2, borderRightWidth: 0 },
]}
>
<Text>Nominal Sisa</Text> <Text>Nominal Sisa</Text>
</View> </View>
</View> </View>
@@ -356,12 +371,7 @@ const createPDFDocument = (
{data.rows.map((item: HppPerKandangRow, index: number) => ( {data.rows.map((item: HppPerKandangRow, index: number) => (
<View <View
key={index} key={index}
style={[ style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}
pdfStyles.tableRow,
index < data.rows.length - 1
? pdfStyles.tableBorderBottom
: {},
]}
> >
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}> <View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
<Text>{index + 1}</Text> <Text>{index + 1}</Text>
@@ -410,11 +420,199 @@ const createPDFDocument = (
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}> <View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
<Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text> <Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text>
</View> </View>
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}> <View
style={[
pdfStyles.tableCellRight,
{ flex: 1.2, borderRightWidth: 0 },
]}
>
<Text>{formatCurrency(item.egg_value_rp)}</Text> <Text>{formatCurrency(item.egg_value_rp)}</Text>
</View> </View>
</View> </View>
))} ))}
{/* TOTAL Row */}
{data.summary?.total && (
<View style={pdfStyles.tableRow}>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 0.5,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>TOTAL</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 1.5,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>ALL</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 1,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>-</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 1,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatNumber(data.summary.total.average_weight_kg)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 0.8,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatNumber(
data.summary.total.total_egg_production_pieces
)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 0.8,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatNumber(data.summary.total.total_egg_production_kg)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 1.2,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{data.rows
.flatMap((row: HppPerKandangRow) =>
row.feed_suppliers?.map(
(s: { alias?: string; name: string }) =>
s.alias || s.name
)
)
.filter(
(v: string, i: number, a: string[]) =>
a.indexOf(v) === i
)
.join(' | ') || '-'}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{
flex: 1,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{data.rows
.flatMap((row: HppPerKandangRow) =>
row.doc_suppliers?.map(
(s: { alias?: string; name: string }) =>
s.alias || s.name
)
)
.filter(
(v: string, i: number, a: string[]) =>
a.indexOf(v) === i
)
.join(' | ') || '-'}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 1.2,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatCurrency(
data.summary.total.total_average_doc_price_rp
)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 1,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
},
]}
>
<Text>
{formatCurrency(
data.summary.total.average_egg_hpp_rp_per_kg
)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellHeaderRight,
{
flex: 1.2,
backgroundColor: '#F5F5F5',
borderBottomWidth: 0,
borderRightWidth: 0,
},
]}
>
<Text>
{formatCurrency(data.summary.total.total_egg_value_rp)}
</Text>
</View>
</View>
)}
</View> </View>
</View> </View>
</Page> </Page>
@@ -40,6 +40,9 @@ const HppPerKandangTab = () => {
// ===== SUBMISSION STATE ===== // ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
// ===== VALIDATION STATE =====
const [weightMaxError, setWeightMaxError] = useState<string>('');
// ===== TABLE FILTER STATE ===== // ===== TABLE FILTER STATE =====
const { state: tableFilterState, updateFilter } = useTableFilter({ const { state: tableFilterState, updateFilter } = useTableFilter({
initial: { initial: {
@@ -127,8 +130,12 @@ const HppPerKandangTab = () => {
const val = e.target.value; const val = e.target.value;
updateFilter('weight_min', val ? String(parseFloat(val) || 0) : ''); updateFilter('weight_min', val ? String(parseFloat(val) || 0) : '');
setIsSubmitted(false); setIsSubmitted(false);
if (weightMaxError) {
setWeightMaxError('');
}
}, },
[updateFilter] [updateFilter, weightMaxError]
); );
const weightMaxChangeHandler = useCallback< const weightMaxChangeHandler = useCallback<
@@ -136,10 +143,22 @@ const HppPerKandangTab = () => {
>( >(
(e) => { (e) => {
const val = e.target.value; const val = e.target.value;
updateFilter('weight_max', val ? String(parseFloat(val) || 0) : ''); const weightMax = val ? parseFloat(val) || 0 : 0;
const weightMin = tableFilterState.weight_min
? parseFloat(tableFilterState.weight_min)
: 0;
if (weightMax < weightMin) {
setWeightMaxError('Rentang bobot max tidak boleh lebih kecil dari min');
toast.error('Rentang bobot max tidak boleh lebih kecil dari min');
return;
}
setWeightMaxError('');
updateFilter('weight_max', val ? String(weightMax) : '');
setIsSubmitted(false); setIsSubmitted(false);
}, },
[updateFilter] [updateFilter, tableFilterState.weight_min]
); );
const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>( const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>(
@@ -325,8 +344,53 @@ const HppPerKandangTab = () => {
const allExportData = const allExportData =
allDataForExport.rows as HppPerKandangReport['rows']; allDataForExport.rows as HppPerKandangReport['rows'];
const perWeightRangeSummary =
allDataForExport.summary.per_weight_range || [];
const summaryTotal = allDataForExport.summary.total; const summaryTotal = allDataForExport.summary.total;
const rekapitulasiData: { [key: string]: string | number }[] =
perWeightRangeSummary.map(
(item: HppPerKandangPerWeightRange, index: number) => ({
No: index + 1,
'Rentang BW': item.label || '',
'Sisa Butir': item.egg_production_pieces || 0,
'Sisa Kg': item.egg_production_kg || 0,
'Rata-Rata Bobot (Kg)': item.avg_weight_kg || 0,
'Feed (Supplier)':
item.feed_suppliers
?.map(
(s: { alias?: string; name: string }) => s.alias || s.name
)
.join(' | ') || '',
'DOC (Supplier)':
item.doc_suppliers
?.map(
(s: { alias?: string; name: string }) => s.alias || s.name
)
.join(' | ') || '',
'Rata-Rata Harga DOC': item.average_doc_price_rp || 0,
'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0,
'Nominal Sisa': item.egg_value_rp || 0,
})
);
const rekapitulasiWorksheet = XLSX.utils.json_to_sheet(rekapitulasiData);
const rekapitulasiColWidths = [
{ wch: 5 }, // No
{ wch: 15 }, // Rentang BW
{ wch: 15 }, // Sisa Butir
{ wch: 12 }, // Sisa Kg
{ wch: 18 }, // Rata-Rata Bobot (Kg)
{ wch: 20 }, // Feed (Supplier)
{ wch: 20 }, // DOC (Supplier)
{ wch: 20 }, // Rata-Rata Harga DOC
{ wch: 18 }, // HPP Telur (RP/KG)
{ wch: 25 }, // Nominal Sisa
];
rekapitulasiWorksheet['!cols'] = rekapitulasiColWidths;
const excelData: { [key: string]: string | number }[] = allExportData.map( const excelData: { [key: string]: string | number }[] = allExportData.map(
(item: HppPerKandangRow, index: number) => ({ (item: HppPerKandangRow, index: number) => ({
No: index + 1, No: index + 1,
@@ -384,7 +448,12 @@ const HppPerKandangTab = () => {
worksheet['!cols'] = colWidths; worksheet['!cols'] = colWidths;
const workbook = XLSX.utils.book_new(); const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang'); XLSX.utils.book_append_sheet(
workbook,
rekapitulasiWorksheet,
'Rekapitulasi'
);
XLSX.utils.book_append_sheet(workbook, worksheet, 'Detail Per Kandang');
const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`; const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`;
@@ -741,6 +810,8 @@ const HppPerKandangTab = () => {
onInputChange={setAreaInputValue} onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas} onMenuScrollToBottom={loadMoreAreas}
isLoading={isLoadingAreas} isLoading={isLoadingAreas}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isClearable isClearable
/> />
<SelectInput <SelectInput
@@ -757,6 +828,8 @@ const HppPerKandangTab = () => {
onInputChange={setLocationInputValue} onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations} onMenuScrollToBottom={loadMoreLocations}
isLoading={isLoadingLocations} isLoading={isLoadingLocations}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isClearable isClearable
/> />
<SelectInput <SelectInput
@@ -773,6 +846,8 @@ const HppPerKandangTab = () => {
onInputChange={setKandangInputValue} onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandangs} onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingKandangs} isLoading={isLoadingKandangs}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isClearable isClearable
/> />
</div> </div>
@@ -792,6 +867,8 @@ const HppPerKandangTab = () => {
placeholder='Masukkan bobot maximum' placeholder='Masukkan bobot maximum'
value={tableFilterState.weight_max} value={tableFilterState.weight_max}
onChange={weightMaxChangeHandler} onChange={weightMaxChangeHandler}
isError={!!weightMaxError}
errorMessage={weightMaxError}
/> />
</div> </div>
<DateInput <DateInput
@@ -818,7 +895,11 @@ const HppPerKandangTab = () => {
</div> </div>
<div className='mt-4 flex justify-end gap-2 [&_button]:px-4'> <div className='mt-4 flex justify-end gap-2 [&_button]:px-4'>
<Button color='primary' onClick={handleSubmit}> <Button
color='primary'
onClick={handleSubmit}
disabled={!!weightMaxError}
>
<Icon icon='heroicons:magnifying-glass' width={20} height={20} /> <Icon icon='heroicons:magnifying-glass' width={20} height={20} />
Cari Cari
</Button> </Button>
+1 -17
View File
@@ -74,23 +74,7 @@ export const RECORDING_APPROVAL_LINE: ApprovalLine = [
}, },
{ {
step_number: 2, step_number: 2,
step_name: 'Approval Head Area', step_name: 'Disetujui',
},
{
step_number: 3,
step_name: 'Approval Business Unit Vice President',
},
{
step_number: 4,
step_name: 'Approval Finance',
},
{
step_number: 5,
step_name: 'Realisasi',
},
{
step_number: 6,
step_name: 'Selesai',
}, },
] as const; ] as const;
+2 -2
View File
@@ -12,7 +12,7 @@ export class FinanceApiService extends BaseApiService<
} }
async getCustomerPaymentReport( async getCustomerPaymentReport(
customer_id?: string, customer_ids?: string,
// TODO: Uncomment when BE is ready // TODO: Uncomment when BE is ready
// sales_id?: string, // sales_id?: string,
// filter_by?: 'do_date', // filter_by?: 'do_date',
@@ -28,7 +28,7 @@ export class FinanceApiService extends BaseApiService<
{ {
method: 'GET', method: 'GET',
params: { params: {
customer_id: customer_id, customer_ids: customer_ids,
// TODO: Uncomment when BE is ready // TODO: Uncomment when BE is ready
// sales_id: sales_id, // sales_id: sales_id,
// filter_by: filter_by, // filter_by: filter_by,
+4
View File
@@ -1,6 +1,8 @@
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general'; import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse'; import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Warehouse } from '@/types/api/master-data/warehouse'; import { Warehouse } from '@/types/api/master-data/warehouse';
import { Kandang } from '@/types/api/master-data/kandang';
import { Location } from '@/types/api/master-data/location';
export type ProductionStandard = { export type ProductionStandard = {
id: number; id: number;
@@ -87,6 +89,8 @@ export type Recording = BaseMetadata &
approval?: BaseApproval; approval?: BaseApproval;
created_user: User; created_user: User;
warehouse?: Warehouse; warehouse?: Warehouse;
kandang?: Kandang;
location?: Location;
product_category?: 'GROWING' | 'LAYING'; product_category?: 'GROWING' | 'LAYING';
depletions?: RecordingDepletion[]; depletions?: RecordingDepletion[];
stocks?: RecordingStock[]; stocks?: RecordingStock[];
+1 -1
View File
@@ -11,7 +11,7 @@ export type CustomerPaymentRow = {
qty: number; qty: number;
weight: number; weight: number;
average_weight: number; average_weight: number;
price: number; unit_price: number;
final_price: number; final_price: number;
total_price: number; total_price: number;
payment_amount: number; payment_amount: number;