mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
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:
@@ -82,12 +82,12 @@ const SalesReportTable = ({
|
||||
<div className='font-semibold text-gray-900'>Total Penjualan</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'age',
|
||||
accessorKey: 'age',
|
||||
header: 'Umur',
|
||||
cell: (props) => props.getValue() || '-',
|
||||
},
|
||||
// {
|
||||
// id: 'age',
|
||||
// accessorKey: 'age',
|
||||
// header: 'Umur',
|
||||
// cell: (props) => props.getValue() || '-',
|
||||
// },
|
||||
{
|
||||
id: 'do_number',
|
||||
accessorKey: 'do_number',
|
||||
|
||||
@@ -43,7 +43,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||
|
||||
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'>
|
||||
<Button
|
||||
href='/expense'
|
||||
@@ -65,7 +65,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||
tabs={expenseDetailTabs}
|
||||
variant='lifted'
|
||||
className={{
|
||||
wrapper: 'max-w-5xl mx-auto mt-4',
|
||||
wrapper: 'mx-auto mt-4',
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -68,7 +68,7 @@ const ExpenseRealizationContent = ({
|
||||
|
||||
return (
|
||||
<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'>
|
||||
<RequirePermission permissions='lti.expense.update.realization'>
|
||||
<Button
|
||||
@@ -84,7 +84,7 @@ const ExpenseRealizationContent = ({
|
||||
</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'>
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -179,7 +179,7 @@ const ExpenseRealizationContent = ({
|
||||
</table>
|
||||
</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'>
|
||||
<Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}>
|
||||
<div className='w-full flex flex-col gap-2'>
|
||||
@@ -216,127 +216,141 @@ const ExpenseRealizationContent = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
||||
<h2 className='font-bold text-xl text-center'>
|
||||
Rincian Pengajuan Biaya Operasional
|
||||
</h2>
|
||||
<div className='w-full mt-8 mx-auto grid grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<h2 className='font-bold text-xl text-center'>
|
||||
Rincian Pengajuan Biaya Operasional
|
||||
</h2>
|
||||
|
||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
||||
let expenseGrandTotal = 0;
|
||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||
{initialValues?.kandangs.map(
|
||||
(kandangExpense, kandangExpenseIdx) => {
|
||||
let expenseGrandTotal = 0;
|
||||
|
||||
kandangExpense.pengajuans?.forEach(
|
||||
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||
);
|
||||
kandangExpense.pengajuans?.forEach(
|
||||
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={kandangExpenseIdx}
|
||||
className='overflow-x-auto w-full mx-auto'
|
||||
>
|
||||
<table className='table table-sm table-zebra'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
colSpan={5}
|
||||
className='font-bold text-center text-base-content text-lg'
|
||||
>
|
||||
Biaya {kandangExpense.name}
|
||||
</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>
|
||||
return (
|
||||
<div
|
||||
key={kandangExpenseIdx}
|
||||
className='overflow-x-auto w-full mx-auto'
|
||||
>
|
||||
<table className='table table-sm table-zebra'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
colSpan={5}
|
||||
className='font-bold text-center text-base-content text-lg'
|
||||
>
|
||||
Biaya {kandangExpense.name}
|
||||
</th>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
<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>
|
||||
)
|
||||
)}
|
||||
</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 className='w-full max-w-5xl mt-8 mx-auto'>
|
||||
<h2 className='font-bold text-xl text-center'>
|
||||
Rincian Realisasi Biaya Operasional
|
||||
</h2>
|
||||
<div>
|
||||
<h2 className='font-bold text-xl text-center'>
|
||||
Rincian Realisasi Biaya Operasional
|
||||
</h2>
|
||||
|
||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
||||
let expenseGrandTotal = 0;
|
||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||
{initialValues?.kandangs.map(
|
||||
(kandangExpense, kandangExpenseIdx) => {
|
||||
let expenseGrandTotal = 0;
|
||||
|
||||
kandangExpense.realisasi?.forEach(
|
||||
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||
);
|
||||
kandangExpense.realisasi?.forEach(
|
||||
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={kandangExpenseIdx}
|
||||
className='overflow-x-auto w-full mx-auto'
|
||||
>
|
||||
<table className='table table-sm table-zebra'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
colSpan={5}
|
||||
className='font-bold text-center text-base-content text-lg'
|
||||
>
|
||||
Biaya {kandangExpense.name}
|
||||
</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>
|
||||
return (
|
||||
<div
|
||||
key={kandangExpenseIdx}
|
||||
className='overflow-x-auto w-full mx-auto'
|
||||
>
|
||||
<table className='table table-sm table-zebra'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
colSpan={5}
|
||||
className='font-bold text-center text-base-content text-lg'
|
||||
>
|
||||
Biaya {kandangExpense.name}
|
||||
</th>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
<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>
|
||||
)
|
||||
)}
|
||||
</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>
|
||||
|
||||
@@ -273,7 +273,7 @@ const ExpenseRequestContent = ({
|
||||
<>
|
||||
<div>
|
||||
{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} />
|
||||
</div>
|
||||
)}
|
||||
@@ -281,7 +281,7 @@ const ExpenseRequestContent = ({
|
||||
<div className='w-full mt-4 flex flex-col gap-4'>
|
||||
{/* 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 && (
|
||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||
<Button
|
||||
@@ -414,7 +414,7 @@ const ExpenseRequestContent = ({
|
||||
</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'>
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -608,7 +608,7 @@ const ExpenseRequestContent = ({
|
||||
</table>
|
||||
</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'>
|
||||
Rincian Pengajuan Biaya Operasional
|
||||
</h2>
|
||||
|
||||
@@ -54,17 +54,19 @@ const RowOptionsMenu = ({
|
||||
rejectClickHandler: () => void;
|
||||
deleteClickHandler: () => void;
|
||||
}) => {
|
||||
const showEditButton =
|
||||
props.row.original.latest_approval.step_number !== 6 &&
|
||||
(props.row.original.latest_approval.step_number === 1 ||
|
||||
props.row.original.latest_approval.step_number === 2 ||
|
||||
props.row.original.latest_approval.step_number === 3 ||
|
||||
props.row.original.latest_approval.step_number === 4);
|
||||
const showEditButton = props.row.original.latest_approval
|
||||
? props.row.original.latest_approval.step_number !== 6 &&
|
||||
(props.row.original.latest_approval.step_number === 1 ||
|
||||
props.row.original.latest_approval.step_number === 2 ||
|
||||
props.row.original.latest_approval.step_number === 3 ||
|
||||
props.row.original.latest_approval.step_number === 4)
|
||||
: false;
|
||||
|
||||
// TODO: apply RBAC
|
||||
const showRealizationButton =
|
||||
props.row.original.latest_approval.action !== 'REJECTED' &&
|
||||
props.row.original.latest_approval.step_number === 4;
|
||||
const showRealizationButton = props.row.original.latest_approval
|
||||
? props.row.original.latest_approval.action !== 'REJECTED' &&
|
||||
props.row.original.latest_approval.step_number === 4
|
||||
: false;
|
||||
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
@@ -278,6 +280,7 @@ const ExpensesTable = () => {
|
||||
cell: ({ row }) => {
|
||||
const isCheckboxDisabled =
|
||||
!row.getCanSelect() ||
|
||||
!row.original.latest_approval ||
|
||||
row.original.latest_approval.action === 'REJECTED';
|
||||
|
||||
return (
|
||||
@@ -413,6 +416,8 @@ const ExpensesTable = () => {
|
||||
const tableEnableRowSelectionHandler: (row: Row<Expense>) => boolean = (
|
||||
row
|
||||
) => {
|
||||
if (!row.original.latest_approval) return false;
|
||||
|
||||
return (
|
||||
row.original.latest_approval.action !== 'REJECTED' &&
|
||||
row.original.latest_approval.step_number !== 6
|
||||
@@ -692,14 +697,6 @@ const ExpensesTable = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Biaya Operasional'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-12 justify-end gap-2'>
|
||||
@@ -753,17 +750,12 @@ const ExpensesTable = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Baris'
|
||||
options={ROWS_OPTIONS}
|
||||
value={{
|
||||
label: String(tableFilterState.pageSize),
|
||||
value: tableFilterState.pageSize,
|
||||
}}
|
||||
onChange={pageSizeChangeHandler}
|
||||
className={{
|
||||
wrapper: 'col-span-12 max-w-28 justify-self-end',
|
||||
}}
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Biaya Operasional'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'col-span-12 max-w-52 justify-self-end' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { isResponseSuccess } from '@/lib/api-helper';
|
||||
interface ExpenseKandangsTableProps {
|
||||
locationId?: number;
|
||||
type: 'add' | 'edit' | 'detail';
|
||||
formType?: 'request' | 'realization';
|
||||
selectedKandangs: {
|
||||
id?: number;
|
||||
name?: string;
|
||||
@@ -31,6 +32,7 @@ interface ExpenseKandangsTableProps {
|
||||
|
||||
const ExpenseKandangsTable = ({
|
||||
type,
|
||||
formType = 'request',
|
||||
locationId,
|
||||
selectedKandangs,
|
||||
onChange,
|
||||
@@ -173,68 +175,76 @@ const ExpenseKandangsTable = ({
|
||||
}, [sorting, updateSortingFilter]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: className?.wrapper,
|
||||
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}
|
||||
<>
|
||||
{selectedKandangs.length > 0 && selectedKandangs.some((k) => k.id) && (
|
||||
<Card
|
||||
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,
|
||||
}),
|
||||
wrapper: className?.wrapper,
|
||||
body: 'p-4 shadow',
|
||||
}}
|
||||
/>
|
||||
</Collapse>
|
||||
</Card>
|
||||
>
|
||||
<Collapse
|
||||
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')
|
||||
: undefined,
|
||||
kandangs: initialValues?.kandangs.map((kandang) => ({
|
||||
id: kandang.kandang_id,
|
||||
id: kandang.id,
|
||||
name: kandang.name,
|
||||
})),
|
||||
supplier: initialValues?.supplier
|
||||
|
||||
@@ -249,7 +249,7 @@ const ExpenseRealizationForm = ({
|
||||
}, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]);
|
||||
|
||||
return (
|
||||
<section className='w-full max-w-5xl'>
|
||||
<section className='w-full'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/expense'
|
||||
@@ -297,6 +297,7 @@ const ExpenseRealizationForm = ({
|
||||
|
||||
<ExpenseKandangsTable
|
||||
type='detail'
|
||||
formType='realization'
|
||||
locationId={formik.values.location?.value}
|
||||
selectedKandangs={formik.values.kandangs ?? []}
|
||||
onChange={kandangsChangeHandler}
|
||||
|
||||
@@ -41,22 +41,25 @@ type ExpenseFormSchemaType = {
|
||||
export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
||||
Yup.object({
|
||||
category: Yup.object({
|
||||
value: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
|
||||
label: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
|
||||
value: Yup.string()
|
||||
.oneOf(['BOP', 'NON-BOP'])
|
||||
.required('Kategori wajib diisi!'),
|
||||
label: Yup.string()
|
||||
.oneOf(['BOP', 'NON-BOP'])
|
||||
.required('Kategori wajib diisi!'),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
.required('Kategori wajib diisi!')
|
||||
.typeError('Kategori wajib diisi!'),
|
||||
|
||||
location: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
}).nullable(),
|
||||
|
||||
location_id: Yup.number()
|
||||
.required('Lokasi wajib diisi!')
|
||||
.min(1, 'Lokasi wajib diisi!')
|
||||
.required('Lokasi wajib diisi!')
|
||||
.typeError('Lokasi wajib diisi!'),
|
||||
|
||||
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
|
||||
@@ -73,9 +76,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
||||
supplier: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
}).nullable(),
|
||||
|
||||
supplier_id: Yup.number()
|
||||
.required('Vendor wajib diisi!')
|
||||
@@ -104,9 +105,12 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
||||
.of(
|
||||
Yup.object({
|
||||
nonstock: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
value: Yup.number().min(1).required('Nonstock wajib diisi!'),
|
||||
label: Yup.string().required('Nonstock wajib diisi!'),
|
||||
})
|
||||
.nullable()
|
||||
.required('Nonstock wajib diisi!')
|
||||
.typeError('Nonstock wajib diisi!'),
|
||||
nonstock_id: Yup.number()
|
||||
.required('Nonstock wajib diisi!')
|
||||
.min(1, 'Nonstock wajib diisi!')
|
||||
|
||||
@@ -190,30 +190,18 @@ const ExpenseRequestForm = ({
|
||||
formik.setFieldValue('category', val);
|
||||
};
|
||||
|
||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('location', true);
|
||||
formik.setFieldValue('location', val);
|
||||
const locationChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const location = val as OptionType | null;
|
||||
const locationId = location ? Number(location.value) : 0;
|
||||
|
||||
const locationId = Array.isArray(val) ? val[0]?.value : val?.value;
|
||||
formik.setFieldValue('location_id', locationId);
|
||||
|
||||
formik.setFieldValue('kandangs', []);
|
||||
|
||||
// Auto-create expense item for location (without kandang)
|
||||
formik.setFieldValue('expense_nonstocks', [
|
||||
{
|
||||
cost_items: [
|
||||
{
|
||||
nonstock: null,
|
||||
nonstock_id: 0,
|
||||
quantity: undefined,
|
||||
price: undefined,
|
||||
notes: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
};
|
||||
formik.setFieldTouched('location', true);
|
||||
formik.setFieldValue('location', location);
|
||||
formik.setFieldTouched('location_id', true);
|
||||
formik.setFieldValue('location_id', locationId);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const kandangsChangeHandler = (
|
||||
kandangs: { id?: number; name?: string }[]
|
||||
@@ -268,6 +256,7 @@ const ExpenseRequestForm = ({
|
||||
|
||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('supplier', true);
|
||||
formik.setFieldTouched('supplier_id', true);
|
||||
formik.setFieldValue('supplier', val);
|
||||
|
||||
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
|
||||
@@ -360,7 +349,7 @@ const ExpenseRequestForm = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full max-w-5xl'>
|
||||
<section className='w-full'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/expense'
|
||||
@@ -407,6 +396,16 @@ const ExpenseRequestForm = ({
|
||||
placeholder='Pilih Kategori'
|
||||
value={formik.values.category}
|
||||
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={[
|
||||
{
|
||||
value: 'BOP',
|
||||
@@ -427,8 +426,13 @@ const ExpenseRequestForm = ({
|
||||
value={formik.values.location}
|
||||
onChange={locationChangeHandler}
|
||||
options={locationOptions}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
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' }}
|
||||
/>
|
||||
|
||||
@@ -438,6 +442,12 @@ const ExpenseRequestForm = ({
|
||||
required
|
||||
value={formik.values.transaction_date}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.transaction_date &&
|
||||
Boolean(formik.errors.transaction_date)
|
||||
}
|
||||
errorMessage={formik.errors.transaction_date as string}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-4',
|
||||
}}
|
||||
@@ -460,8 +470,12 @@ const ExpenseRequestForm = ({
|
||||
value={formik.values.supplier}
|
||||
onChange={supplierChangeHandler}
|
||||
options={supplierOptions}
|
||||
isLoading={isLoadingVendorOptions}
|
||||
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' }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||
true
|
||||
);
|
||||
formik.setFieldTouched(
|
||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||
val
|
||||
@@ -96,7 +100,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
};
|
||||
|
||||
const isExpenseRepeaterInputError = (
|
||||
column: 'nonstock' | 'quantity' | 'price' | 'notes',
|
||||
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
||||
kandangExpenseIdx: number,
|
||||
expenseIdx: number
|
||||
) => {
|
||||
@@ -105,11 +109,14 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
expenseIdx
|
||||
]?.[column] &&
|
||||
Boolean(
|
||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof
|
||||
Object &&
|
||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx] &&
|
||||
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx] ===
|
||||
'object' &&
|
||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||
expenseIdx
|
||||
] instanceof Object &&
|
||||
] &&
|
||||
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx]
|
||||
.cost_items?.[expenseIdx] === 'object' &&
|
||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||
expenseIdx
|
||||
]?.[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 (
|
||||
<Card
|
||||
className={{
|
||||
@@ -202,10 +235,21 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
val
|
||||
);
|
||||
}}
|
||||
isError={isExpenseRepeaterInputError(
|
||||
'nonstock_id',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
errorMessage={getExpenseRepeaterErrorMessage(
|
||||
'nonstock_id',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
options={nonstockOptions}
|
||||
isLoading={isLoadingNonstockOptions}
|
||||
onInputChange={setNonstockInputValue}
|
||||
className={{ wrapper: 'min-w-48' }}
|
||||
isClearable={true}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -226,6 +270,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
errorMessage={getExpenseRepeaterErrorMessage(
|
||||
'quantity',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
className={{ wrapper: 'min-w-24' }}
|
||||
/>
|
||||
</td>
|
||||
@@ -246,6 +295,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
errorMessage={getExpenseRepeaterErrorMessage(
|
||||
'price',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
inputPrefix={
|
||||
<span className='text-gray-600 font-medium'>
|
||||
Rp
|
||||
@@ -271,6 +325,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
errorMessage={getExpenseRepeaterErrorMessage(
|
||||
'notes',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
className={{ wrapper: 'min-w-24' }}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -110,6 +110,14 @@ const DeliveryProductObjectSchema = Yup.object({
|
||||
.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({
|
||||
delivery_cost: Yup.number()
|
||||
.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_index: Yup.number().optional(),
|
||||
document: Yup.mixed<File | MovementDocument>()
|
||||
.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;
|
||||
}),
|
||||
document: DeliveryDocumentSchema,
|
||||
driver_name: Yup.string().required('Nama sopir wajib diisi!'),
|
||||
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
|
||||
supplier: Yup.object({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import useSWR from 'swr';
|
||||
|
||||
@@ -95,7 +95,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
isLoadingOptions: isLoadingWarehouses,
|
||||
loadMore: loadMoreWarehouses,
|
||||
rawData: warehouses,
|
||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search');
|
||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search', {
|
||||
flag: 'EKSPEDISI',
|
||||
});
|
||||
|
||||
// ===== SELECT INPUT DATA =====
|
||||
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) =====
|
||||
const {
|
||||
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 =====
|
||||
// Product Handlers
|
||||
const addProduct = () => {
|
||||
const handleTransferDateChange = useCallback(
|
||||
(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 = [
|
||||
...(formik.values.products || []),
|
||||
{
|
||||
@@ -363,22 +464,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
},
|
||||
];
|
||||
formik.setFieldValue('products', newProducts);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const removeProduct = useCallback(
|
||||
(i: number) => {
|
||||
const updatedProducts =
|
||||
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
||||
if (index !== i) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, []) ?? [];
|
||||
const removeProduct = useCallback((i: number) => {
|
||||
const updatedProducts =
|
||||
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
||||
if (index !== i) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, []) ?? [];
|
||||
|
||||
formik.setFieldValue('products', updatedProducts);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
formik.setFieldValue('products', updatedProducts);
|
||||
}, []);
|
||||
|
||||
const bulkRemoveProduct = useCallback(() => {
|
||||
const updatedProducts =
|
||||
@@ -387,10 +485,45 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
) ?? [];
|
||||
formik.setFieldValue('products', updatedProducts);
|
||||
setSelectedProducts([]);
|
||||
}, [formik, selectedProducts]);
|
||||
}, [formik, selectedProducts, setSelectedProducts]);
|
||||
|
||||
// Delivery Handlers
|
||||
const addDelivery = () => {
|
||||
const handleProductChange = useCallback(
|
||||
(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.values.deliveries || []),
|
||||
{
|
||||
@@ -410,25 +543,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
],
|
||||
},
|
||||
]);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const removeDelivery = useCallback(
|
||||
(i: number) => {
|
||||
const updatedDeliveries =
|
||||
formik.values.deliveries?.reduce(
|
||||
(acc: DeliverySchema[], item, index) => {
|
||||
if (index !== i) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
) ?? [];
|
||||
const removeDelivery = useCallback((i: number) => {
|
||||
const updatedDeliveries =
|
||||
formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => {
|
||||
if (index !== i) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, []) ?? [];
|
||||
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
}, []);
|
||||
|
||||
const bulkRemoveDelivery = useCallback(() => {
|
||||
const updatedDeliveries =
|
||||
@@ -437,33 +564,101 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
) ?? [];
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
setSelectedDeliveries([]);
|
||||
}, [formik, selectedDeliveries]);
|
||||
}, [formik, selectedDeliveries, setSelectedDeliveries]);
|
||||
|
||||
// Cost Calculation Handlers
|
||||
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
|
||||
const handleDeliverySelectAllChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedDeliveries(
|
||||
formik.values.deliveries?.map((_, idx) => idx) ?? []
|
||||
);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
setSelectedDeliveries([]);
|
||||
}
|
||||
},
|
||||
[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(
|
||||
(idx: number, value: number) => {
|
||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
||||
@@ -482,7 +677,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[formik]
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeliveryCostChangeWrapper = useCallback(
|
||||
@@ -957,43 +1152,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
label='Gudang'
|
||||
placeholder='Pilih gudang asal...'
|
||||
value={formik.values.source_warehouse}
|
||||
onChange={(val) => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
onChange={handleSourceWarehouseChange}
|
||||
options={warehouseOptions}
|
||||
onInputChange={setWarehouseSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreWarehouses}
|
||||
@@ -1057,41 +1216,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
label='Gudang'
|
||||
placeholder='Pilih gudang tujuan...'
|
||||
value={formik.values.destination_warehouse}
|
||||
onChange={(val) => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
onChange={handleDestinationWarehouseChange}
|
||||
options={warehouseOptions}
|
||||
onInputChange={setWarehouseSelectInputValue}
|
||||
isLoading={isLoadingWarehouses}
|
||||
@@ -1165,18 +1290,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
selectedProducts.length &&
|
||||
formik.values.products?.length > 0
|
||||
}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedProducts(
|
||||
formik.values.products?.map((_, idx) => idx) ??
|
||||
[]
|
||||
);
|
||||
} else {
|
||||
setSelectedProducts([]);
|
||||
}
|
||||
}}
|
||||
onChange={handleProductSelectAllChange}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -1213,17 +1327,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
<CheckboxInput
|
||||
name={`product-${idx}`}
|
||||
checked={selectedProducts.includes(idx)}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedProducts([...selectedProducts, idx]);
|
||||
} else {
|
||||
setSelectedProducts(
|
||||
selectedProducts.filter((i) => i !== idx)
|
||||
);
|
||||
}
|
||||
}}
|
||||
onChange={handleProductCheckboxChange}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -1235,24 +1339,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
<SelectInput
|
||||
required
|
||||
value={product.product ?? undefined}
|
||||
onChange={(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
|
||||
);
|
||||
}}
|
||||
onChange={(val) => handleProductChange(idx, val)}
|
||||
options={productWarehouseOptions}
|
||||
onInputChange={setProductWarehouseSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreProductWarehouses}
|
||||
@@ -1379,19 +1466,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
selectedDeliveries.length &&
|
||||
formik.values.deliveries?.length > 0
|
||||
}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedDeliveries(
|
||||
formik.values.deliveries?.map(
|
||||
(_, idx) => idx
|
||||
) ?? []
|
||||
);
|
||||
} else {
|
||||
setSelectedDeliveries([]);
|
||||
}
|
||||
}}
|
||||
onChange={handleDeliverySelectAllChange}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -1474,20 +1549,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
<CheckboxInput
|
||||
name={`delivery-${idx}`}
|
||||
checked={selectedDeliveries.includes(idx)}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedDeliveries([
|
||||
...selectedDeliveries,
|
||||
idx,
|
||||
]);
|
||||
} else {
|
||||
setSelectedDeliveries(
|
||||
selectedDeliveries.filter((i) => i !== idx)
|
||||
);
|
||||
}
|
||||
}}
|
||||
onChange={handleDeliveryCheckboxChange}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -1500,24 +1562,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
required
|
||||
placeholder='Pilih produk...'
|
||||
value={delivery.products[0]?.product ?? undefined}
|
||||
onChange={(val) => {
|
||||
formik.setFieldTouched(
|
||||
`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
|
||||
);
|
||||
}}
|
||||
onChange={(val) =>
|
||||
handleDeliveryProductChange(idx, val)
|
||||
}
|
||||
options={getFilteredProductWarehouseOptions()}
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable
|
||||
@@ -1568,24 +1615,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
required
|
||||
placeholder='Pilih supplier...'
|
||||
value={delivery.supplier}
|
||||
onChange={(val) => {
|
||||
formik.setFieldTouched(
|
||||
`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
|
||||
);
|
||||
}}
|
||||
onChange={(val) =>
|
||||
handleDeliverySupplierChange(idx, val)
|
||||
}
|
||||
options={supplierOptions}
|
||||
onInputChange={setSupplierSelectInputValue}
|
||||
isLoading={isLoadingSuppliers}
|
||||
@@ -1677,20 +1709,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
<FileInput
|
||||
accept='.pdf,.jpg,.jpeg,.png'
|
||||
name={`deliveries.${idx}.document`}
|
||||
onChange={(e) => {
|
||||
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.${idx}.document`,
|
||||
file
|
||||
);
|
||||
}
|
||||
}}
|
||||
onChange={(e) =>
|
||||
handleDeliveryDocumentChange(idx, e)
|
||||
}
|
||||
{...isRepeaterInputError(
|
||||
'deliveries',
|
||||
'document',
|
||||
|
||||
@@ -686,10 +686,18 @@ const RecordingTable = () => {
|
||||
1,
|
||||
},
|
||||
{
|
||||
header: 'Nama Project',
|
||||
header: 'Lokasi',
|
||||
cell: (props) => props.row.original.location?.name || '-',
|
||||
},
|
||||
{
|
||||
header: 'Flock',
|
||||
cell: (props) =>
|
||||
props.row.original.project_flock?.flock_name || '-',
|
||||
},
|
||||
{
|
||||
header: 'Kandang',
|
||||
cell: (props) => props.row.original.kandang?.name || '-',
|
||||
},
|
||||
{
|
||||
header: 'Periode',
|
||||
cell: (props) => props.row.original.project_flock?.period || '-',
|
||||
@@ -722,12 +730,10 @@ const RecordingTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'warehouse.name',
|
||||
header: 'Gudang',
|
||||
cell: (props) => props.row.original.warehouse?.name,
|
||||
},
|
||||
{
|
||||
accessorKey: 'record_date',
|
||||
header: 'Waktu Recording',
|
||||
cell: (props) =>
|
||||
formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'),
|
||||
|
||||
@@ -300,7 +300,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
<Text>Rata-Rata</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Harga Awal</Text>
|
||||
<Text>Harga/Unit</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Harga Akhir</Text>
|
||||
@@ -378,7 +378,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
<Text>{formatNumber(item.average_weight)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.price)}</Text>
|
||||
<Text>{formatCurrency(item.unit_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.final_price)}</Text>
|
||||
|
||||
@@ -38,7 +38,7 @@ export const generateCustomerPaymentExcel = (
|
||||
'Ekor/Qty': formatNumber(item.qty || 0),
|
||||
'Berat (Kg)': formatNumber(item.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),
|
||||
Total: formatCurrency(item.total_price || 0),
|
||||
Pembayaran: formatCurrency(item.payment_amount || 0),
|
||||
@@ -62,7 +62,7 @@ export const generateCustomerPaymentExcel = (
|
||||
'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0),
|
||||
'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0),
|
||||
AVG: '',
|
||||
'Harga Awal': '',
|
||||
'Harga/Unit': '',
|
||||
'Harga Akhir': formatCurrency(
|
||||
customerReport.summary.total_final_amount || 0
|
||||
),
|
||||
@@ -89,7 +89,7 @@ export const generateCustomerPaymentExcel = (
|
||||
{ wch: 10 }, // Ekor/Qty
|
||||
{ wch: 12 }, // Berat
|
||||
{ wch: 10 }, // AVG
|
||||
{ wch: 15 }, // Harga Awal
|
||||
{ wch: 15 }, // Harga/Unit
|
||||
{ wch: 15 }, // Harga Akhir
|
||||
{ wch: 15 }, // Total
|
||||
{ wch: 15 }, // Pembayaran
|
||||
|
||||
@@ -106,7 +106,11 @@ const CustomerPaymentTab = () => {
|
||||
};
|
||||
|
||||
const getPaymentStatusText = (notes: string) => {
|
||||
return notes;
|
||||
return notes
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
@@ -159,7 +163,7 @@ const CustomerPaymentTab = () => {
|
||||
isSubmitted
|
||||
? () => {
|
||||
const params = {
|
||||
customer_id:
|
||||
customer_ids:
|
||||
filterCustomer.length > 0
|
||||
? filterCustomer.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
@@ -180,7 +184,7 @@ const CustomerPaymentTab = () => {
|
||||
: null,
|
||||
([, params]) =>
|
||||
FinanceApi.getCustomerPaymentReport(
|
||||
params.customer_id,
|
||||
params.customer_ids,
|
||||
undefined, // TODO: Change to params.sales_id when BE is ready
|
||||
undefined, // TODO: Change to params.filter_by when BE is ready
|
||||
params.start_date,
|
||||
@@ -203,7 +207,7 @@ const CustomerPaymentTab = () => {
|
||||
CustomerPaymentReport[] | null
|
||||
> => {
|
||||
const params = {
|
||||
customer_id:
|
||||
customer_ids:
|
||||
filterCustomer.length > 0
|
||||
? filterCustomer.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
@@ -219,7 +223,7 @@ const CustomerPaymentTab = () => {
|
||||
};
|
||||
|
||||
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.filter_by when BE is ready
|
||||
params.start_date,
|
||||
@@ -336,7 +340,9 @@ const CustomerPaymentTab = () => {
|
||||
const value = props.row.original.aging_day;
|
||||
return (
|
||||
<div className='text-center'>
|
||||
{value && value > 0 ? `${formatNumber(value)} hari` : '-'}
|
||||
{value !== null && value !== undefined
|
||||
? `${formatNumber(value)} hari`
|
||||
: '-'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -405,12 +411,12 @@ const CustomerPaymentTab = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'price',
|
||||
header: 'Harga Awal',
|
||||
accessorKey: 'price',
|
||||
id: 'unit_price',
|
||||
header: 'Harga/Unit',
|
||||
accessorKey: 'unit_price',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.price;
|
||||
const value = props.row.original.unit_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
@@ -510,7 +516,7 @@ const CustomerPaymentTab = () => {
|
||||
status: getPaymentStatusIndicatorColor(value),
|
||||
}}
|
||||
>
|
||||
<span>{getPaymentStatusText(value)}</span>
|
||||
<span className='capitalize'>{getPaymentStatusText(value)}</span>
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -246,7 +246,12 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>HPP Telur (RP/KG)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{ flex: 1.2, borderRightWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text>Nominal Sisa</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -301,7 +306,12 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{ flex: 1.2, borderRightWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text>{formatCurrency(group.egg_value_rp)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -347,7 +357,12 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>HPP Telur (RP/KG)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{ flex: 1.2, borderRightWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text>Nominal Sisa</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -356,12 +371,7 @@ const createPDFDocument = (
|
||||
{data.rows.map((item: HppPerKandangRow, index: number) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
index < data.rows.length - 1
|
||||
? pdfStyles.tableBorderBottom
|
||||
: {},
|
||||
]}
|
||||
style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}
|
||||
>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
|
||||
<Text>{index + 1}</Text>
|
||||
@@ -410,11 +420,199 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{ flex: 1.2, borderRightWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text>{formatCurrency(item.egg_value_rp)}</Text>
|
||||
</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>
|
||||
</Page>
|
||||
|
||||
@@ -40,6 +40,9 @@ const HppPerKandangTab = () => {
|
||||
// ===== SUBMISSION STATE =====
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// ===== VALIDATION STATE =====
|
||||
const [weightMaxError, setWeightMaxError] = useState<string>('');
|
||||
|
||||
// ===== TABLE FILTER STATE =====
|
||||
const { state: tableFilterState, updateFilter } = useTableFilter({
|
||||
initial: {
|
||||
@@ -127,8 +130,12 @@ const HppPerKandangTab = () => {
|
||||
const val = e.target.value;
|
||||
updateFilter('weight_min', val ? String(parseFloat(val) || 0) : '');
|
||||
setIsSubmitted(false);
|
||||
|
||||
if (weightMaxError) {
|
||||
setWeightMaxError('');
|
||||
}
|
||||
},
|
||||
[updateFilter]
|
||||
[updateFilter, weightMaxError]
|
||||
);
|
||||
|
||||
const weightMaxChangeHandler = useCallback<
|
||||
@@ -136,10 +143,22 @@ const HppPerKandangTab = () => {
|
||||
>(
|
||||
(e) => {
|
||||
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);
|
||||
},
|
||||
[updateFilter]
|
||||
[updateFilter, tableFilterState.weight_min]
|
||||
);
|
||||
|
||||
const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||
@@ -325,8 +344,53 @@ const HppPerKandangTab = () => {
|
||||
const allExportData =
|
||||
allDataForExport.rows as HppPerKandangReport['rows'];
|
||||
|
||||
const perWeightRangeSummary =
|
||||
allDataForExport.summary.per_weight_range || [];
|
||||
|
||||
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(
|
||||
(item: HppPerKandangRow, index: number) => ({
|
||||
No: index + 1,
|
||||
@@ -384,7 +448,12 @@ const HppPerKandangTab = () => {
|
||||
worksheet['!cols'] = colWidths;
|
||||
|
||||
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`;
|
||||
|
||||
@@ -741,6 +810,8 @@ const HppPerKandangTab = () => {
|
||||
onInputChange={setAreaInputValue}
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
isLoading={isLoadingAreas}
|
||||
closeMenuOnSelect={false}
|
||||
hideSelectedOptions={false}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
@@ -757,6 +828,8 @@ const HppPerKandangTab = () => {
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isLoading={isLoadingLocations}
|
||||
closeMenuOnSelect={false}
|
||||
hideSelectedOptions={false}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
@@ -773,6 +846,8 @@ const HppPerKandangTab = () => {
|
||||
onInputChange={setKandangInputValue}
|
||||
onMenuScrollToBottom={loadMoreKandangs}
|
||||
isLoading={isLoadingKandangs}
|
||||
closeMenuOnSelect={false}
|
||||
hideSelectedOptions={false}
|
||||
isClearable
|
||||
/>
|
||||
</div>
|
||||
@@ -792,6 +867,8 @@ const HppPerKandangTab = () => {
|
||||
placeholder='Masukkan bobot maximum'
|
||||
value={tableFilterState.weight_max}
|
||||
onChange={weightMaxChangeHandler}
|
||||
isError={!!weightMaxError}
|
||||
errorMessage={weightMaxError}
|
||||
/>
|
||||
</div>
|
||||
<DateInput
|
||||
@@ -818,7 +895,11 @@ const HppPerKandangTab = () => {
|
||||
</div>
|
||||
|
||||
<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} />
|
||||
Cari
|
||||
</Button>
|
||||
|
||||
@@ -74,23 +74,7 @@ export const RECORDING_APPROVAL_LINE: ApprovalLine = [
|
||||
},
|
||||
{
|
||||
step_number: 2,
|
||||
step_name: 'Approval Head Area',
|
||||
},
|
||||
{
|
||||
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',
|
||||
step_name: 'Disetujui',
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export class FinanceApiService extends BaseApiService<
|
||||
}
|
||||
|
||||
async getCustomerPaymentReport(
|
||||
customer_id?: string,
|
||||
customer_ids?: string,
|
||||
// TODO: Uncomment when BE is ready
|
||||
// sales_id?: string,
|
||||
// filter_by?: 'do_date',
|
||||
@@ -28,7 +28,7 @@ export class FinanceApiService extends BaseApiService<
|
||||
{
|
||||
method: 'GET',
|
||||
params: {
|
||||
customer_id: customer_id,
|
||||
customer_ids: customer_ids,
|
||||
// TODO: Uncomment when BE is ready
|
||||
// sales_id: sales_id,
|
||||
// filter_by: filter_by,
|
||||
|
||||
+4
@@ -1,6 +1,8 @@
|
||||
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
|
||||
import { ProductWarehouse } from '@/types/api/inventory/product-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 = {
|
||||
id: number;
|
||||
@@ -87,6 +89,8 @@ export type Recording = BaseMetadata &
|
||||
approval?: BaseApproval;
|
||||
created_user: User;
|
||||
warehouse?: Warehouse;
|
||||
kandang?: Kandang;
|
||||
location?: Location;
|
||||
product_category?: 'GROWING' | 'LAYING';
|
||||
depletions?: RecordingDepletion[];
|
||||
stocks?: RecordingStock[];
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export type CustomerPaymentRow = {
|
||||
qty: number;
|
||||
weight: number;
|
||||
average_weight: number;
|
||||
price: number;
|
||||
unit_price: number;
|
||||
final_price: number;
|
||||
total_price: number;
|
||||
payment_amount: number;
|
||||
|
||||
Reference in New Issue
Block a user