Merge branch 'development' into fix/transfer-to-laying

This commit is contained in:
ValdiANS
2026-01-20 17:37:06 +07:00
55 changed files with 1824 additions and 923 deletions
+18 -2
View File
@@ -113,7 +113,15 @@ const DateInput = ({
};
const handleSelectSingle = (selectedDate?: Date) => {
if (!selectedDate) return;
if (!selectedDate) {
setSelected(undefined);
setDisplayValue('');
const syntheticEvent = {
target: { name, value: '' },
} as unknown as React.ChangeEvent<HTMLInputElement>;
onChange?.(syntheticEvent);
return;
}
if (minDate && selectedDate < minDate) {
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
return;
@@ -136,7 +144,15 @@ const DateInput = ({
};
const handleSelectRange = (range?: { from?: Date; to?: Date }) => {
if (!range) return;
if (!range) {
setSelectedRange({});
setDisplayValue('');
const syntheticEvent = {
target: { name, value: { from: '', to: '' } },
} as unknown as React.ChangeEvent<HTMLInputElement>;
onChange?.(syntheticEvent);
return;
}
setSelectedRange(range);
const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : '';
@@ -0,0 +1,174 @@
'use client';
import { ChangeEventHandler, useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { ColumnDef, SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Card from '@/components/Card';
import Collapse from '@/components/Collapse';
import { cn, formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ClosingApi } from '@/services/api/closing';
import { ClosingIncomingSapronakSummary } from '@/types/api/closing';
interface ClosingIncomingSapronaksSummaryTableProps {
projectFlockId: number;
}
const ClosingIncomingSapronaksSummaryTable = ({
projectFlockId,
}: ClosingIncomingSapronaksSummaryTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
nameSort: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
nameSort: 'sort_name',
},
});
const {
data: incomingSapronakSummaries,
isLoading: isLoadingIncomingSapronakSummaries,
} = useSWR(
`${ClosingApi.basePath}/${projectFlockId}/sapronak/summary${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`,
ClosingApi.getAllIncomingSapronakSummaryFetcher,
{
keepPreviousData: true,
}
);
const [open, setOpen] = useState(true);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
[
{
header: '#',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'category',
header: 'Kategori',
},
{
accessorKey: 'total_qty',
header: 'Total Kuantitas',
cell: (props) =>
`${formatNumber(props.row.original.total_qty)} ${props.row.original.uom.name}`,
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
useEffect(() => {
if (!open) {
setOpen(
isResponseSuccess(incomingSapronakSummaries)
? incomingSapronakSummaries.data.length > 0
: false
);
}
}, [incomingSapronakSummaries, isResponseSuccess]);
return (
<Card
className={{
wrapper: 'w-full',
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'>Ringkasan Sapronak Masuk</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!'
>
<div className='w-full p-0'>
<Table<ClosingIncomingSapronakSummary>
data={
isResponseSuccess(incomingSapronakSummaries)
? incomingSapronakSummaries?.data
: []
}
columns={incomingSapronaksColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(incomingSapronakSummaries)
? incomingSapronakSummaries?.meta?.page
: 0
}
totalItems={
isResponseSuccess(incomingSapronakSummaries)
? incomingSapronakSummaries?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingIncomingSapronakSummaries}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(incomingSapronakSummaries) &&
incomingSapronakSummaries?.data?.length === 0,
}),
}}
/>
</div>
</Collapse>
</Card>
);
};
export default ClosingIncomingSapronaksSummaryTable;
@@ -0,0 +1,174 @@
'use client';
import { ChangeEventHandler, useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { ColumnDef, SortingState } from '@tanstack/react-table';
import { Icon } from '@iconify/react';
import Table from '@/components/Table';
import Card from '@/components/Card';
import Collapse from '@/components/Collapse';
import { cn, formatNumber } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ClosingApi } from '@/services/api/closing';
import { ClosingOutgoingSapronakSummary } from '@/types/api/closing';
interface ClosingOutgoingSapronaksSummaryTableProps {
projectFlockId: number;
}
const ClosingOutgoingSapronaksSummaryTable = ({
projectFlockId,
}: ClosingOutgoingSapronaksSummaryTableProps) => {
const searchParams = useSearchParams();
const kandangId = searchParams.get('kandangId');
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
nameSort: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
nameSort: 'sort_name',
},
});
const {
data: outgoingSapronakSummaries,
isLoading: isLoadingOutgoingSapronakSummaries,
} = useSWR(
`${ClosingApi.basePath}/${projectFlockId}/sapronak/summary${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
ClosingApi.getAllIncomingSapronakSummaryFetcher,
{
keepPreviousData: true,
}
);
const [open, setOpen] = useState(true);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
[
{
header: '#',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'category',
header: 'Kategori',
},
{
accessorKey: 'total_qty',
header: 'Total Kuantitas',
cell: (props) =>
`${formatNumber(props.row.original.total_qty)} ${props.row.original.uom.name}`,
},
];
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
updateFilter('search', e.target.value);
};
// track sorting
useEffect(() => {
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
if (!isNameSorted) {
updateFilter('nameSort', '');
} else {
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
}
}, [sorting, updateFilter]);
useEffect(() => {
if (!open) {
setOpen(
isResponseSuccess(outgoingSapronakSummaries)
? outgoingSapronakSummaries.data.length > 0
: false
);
}
}, [outgoingSapronakSummaries, isResponseSuccess]);
return (
<Card
className={{
wrapper: 'w-full',
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'>Ringkasan Sapronak Keluar</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!'
>
<div className='w-full p-0'>
<Table<ClosingOutgoingSapronakSummary>
data={
isResponseSuccess(outgoingSapronakSummaries)
? outgoingSapronakSummaries?.data
: []
}
columns={outgoingSapronaksColumns}
pageSize={tableFilterState.pageSize}
onPageSizeChange={setPageSize}
rowOptions={[10, 20, 50, 100]}
page={
isResponseSuccess(outgoingSapronakSummaries)
? outgoingSapronakSummaries?.meta?.page
: 0
}
totalItems={
isResponseSuccess(outgoingSapronakSummaries)
? outgoingSapronakSummaries?.meta?.total_results
: 0
}
onPageChange={setPage}
isLoading={isLoadingOutgoingSapronakSummaries}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
className={{
containerClassName: cn({
'w-full mb-20':
isResponseSuccess(outgoingSapronakSummaries) &&
outgoingSapronakSummaries?.data?.length === 0,
}),
}}
/>
</div>
</Collapse>
</Card>
);
};
export default ClosingOutgoingSapronaksSummaryTable;
@@ -2,6 +2,8 @@
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable';
import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable';
interface ClosingSapronakTableProps {
projectFlockId?: number;
@@ -16,7 +18,15 @@ const ClosingSapronakTabContent = ({
<>
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
<ClosingIncomingSapronaksSummaryTable
projectFlockId={projectFlockId}
/>
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
<ClosingOutgoingSapronaksSummaryTable
projectFlockId={projectFlockId}
/>
</>
)}
</div>
@@ -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',
@@ -253,7 +253,6 @@ export const generateDashboardPDF = async ({
toast.success('PDF exported successfully!', { id: 'export-pdf' });
} catch (error) {
console.error('Error generating PDF:', error);
toast.error('Failed to export PDF. Please try again.', {
id: 'export-pdf',
});
@@ -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.notes ?? '-'}
</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.notes ?? '-'}
</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>
@@ -654,7 +654,7 @@ const ExpenseRequestContent = ({
<td>{pengajuanItem.qty}</td>
<td>{formatCurrency(pengajuanItem.price)}</td>
<td className='w-xs'>
{pengajuanItem.note ?? '-'}
{pengajuanItem.notes ?? '-'}
</td>
</tr>
)
+20 -28
View File
@@ -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,
@@ -172,69 +174,84 @@ const ExpenseKandangsTable = ({
updateSortingFilter('picSort', picSortFilter);
}, [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>
// Tampilkan tabel jika:
// 1. Mode request pertama kali (type='add' dan formType='request')
// 2. Atau sudah ada kandang yang dipilih
const shouldShowTable =
(type === 'add' && formType === 'request') ||
(selectedKandangs.length > 0 && selectedKandangs.some((k) => k.id));
<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}
return (
<>
{shouldShowTable && (
<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
@@ -159,7 +159,7 @@ export const getExpenseRealizationFormInitialValues = (
},
quantity: realisasiItem.qty,
price: realisasiItem.price,
notes: realisasiItem.note,
notes: realisasiItem.notes,
};
})
: kandangExpense.pengajuans
@@ -170,7 +170,7 @@ export const getExpenseRealizationFormInitialValues = (
},
quantity: expenseItem.qty,
price: expenseItem.price,
notes: expenseItem.note,
notes: expenseItem.notes,
}))
: [];
@@ -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!')
@@ -204,7 +208,7 @@ export const getExpenseFormInitialValues = (
nonstock_id: expenseItem.nonstock.id,
quantity: expenseItem.qty,
price: expenseItem.price,
notes: expenseItem.note,
notes: expenseItem.notes,
}))
: [],
}))
@@ -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>
@@ -447,7 +447,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{pengajuan.note}
{pengajuan.notes}
</Text>
</View>
</View>
@@ -607,7 +607,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{realisasi.note}
{realisasi.notes}
</Text>
</View>
</View>
@@ -64,7 +64,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
},
{
label: 'Nominal',
value: formatCurrency(finance.nominal),
value: formatCurrency(Math.abs(finance.nominal)),
},
].filter((item) => {
// Hide party account number row if transaction type is INJECTION
+111 -80
View File
@@ -19,6 +19,7 @@ import {
FINANCE_INITIAL_BALANCE_STATUS,
FINANCE_INJECTION_STATUS,
FINANCE_TRANSACTION_STATUS,
FINANCE_TRANSACTION_TYPE_OPTIONS,
} from '@/config/constant';
import { FinanceApi } from '@/services/api/finance';
import { isResponseSuccess } from '@/lib/api-helper';
@@ -65,24 +66,19 @@ const RowOptionsMenu = ({
{FINANCE_TRANSACTION_STATUS.includes(
props.row.original.transaction_type
) &&
props.row.original.party?.type !== 'SUPPLIER' && (
<RequirePermission permissions='lti.finance.payments.update'>
<Button
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon
icon='material-symbols:edit-outline'
width={16}
height={16}
/>
Edit
</Button>
</RequirePermission>
)}
) && (
<RequirePermission permissions='lti.finance.payments.update'>
<Button
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
)}
{FINANCE_INITIAL_BALANCE_STATUS.includes(
props.row.original.transaction_type
@@ -148,7 +144,8 @@ const FinanceTable = () => {
search: '',
transactionType: '',
bankId: '',
partyType: '',
customerId: '',
supplierId: '',
sortBy: '',
startDate: '',
endDate: '',
@@ -158,7 +155,8 @@ const FinanceTable = () => {
pageSize: 'limit',
transactionType: 'transaction_type',
bankId: 'bank_id',
partyType: 'party_type',
customerId: 'customer_id',
supplierId: 'supplier_id',
sortBy: 'sort_date',
startDate: 'start_date',
endDate: 'end_date',
@@ -172,17 +170,24 @@ const FinanceTable = () => {
search: '',
transactionType: '',
bankId: '',
partyType: '',
customerId: '',
supplierId: '',
sortBy: '',
startDate: '',
endDate: '',
});
const [selectedTransactionType, setSelectedTransactionType] =
useState<OptionType | null>(null);
const [selectedBank, setSelectedBank] = useState<OptionType | null>(null);
const [selectedPartyType, setSelectedPartyType] = useState<OptionType | null>(
null
);
const [selectedTransactionType, setSelectedTransactionType] = useState<
OptionType | OptionType[] | null
>(null);
const [selectedBank, setSelectedBank] = useState<
OptionType | OptionType[] | null
>(null);
const [selectedCustomerId, setSelectedCustomerId] = useState<
OptionType | OptionType[] | null
>(null);
const [selectedSupplierId, setSelectedSupplierId] = useState<
OptionType | OptionType[] | null
>(null);
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
@@ -197,27 +202,18 @@ const FinanceTable = () => {
FinanceApi.getAllFetcher
);
// ===== Options =====
const transactionTypeOptions = useMemo(() => {
return [
{ label: 'Customer', value: 'CUSTOMER' },
{ label: 'Supplier', value: 'SUPPLIER' },
];
}, []);
const {
options: partyTypeOptions,
isLoadingOptions: partyTypeIsLoadingOptions,
setInputValue: partyTypeInputValue,
loadMore: partyTypeLoadMore,
} = useSelect(
selectedTransactionType
? selectedTransactionType.value === 'CUSTOMER'
? CustomerApi.basePath
: SupplierApi.basePath
: '',
'id',
'name'
);
options: customerOptions,
isLoadingOptions: customerIsLoadingOptions,
setInputValue: customerInputValue,
loadMore: customerLoadMore,
} = useSelect(CustomerApi.basePath, 'id', 'name');
const {
options: supplierOptions,
isLoadingOptions: supplierIsLoadingOptions,
setInputValue: supplierInputValue,
loadMore: supplierLoadMore,
} = useSelect(SupplierApi.basePath, 'id', 'name');
const sortByOptions = useMemo(() => {
return [
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
@@ -238,24 +234,47 @@ const FinanceTable = () => {
const transactionTypeChangeHandler = (
val: OptionType | OptionType[] | null
) => {
setSelectedTransactionType(val as OptionType);
setSelectedTransactionType(val);
setPendingFilters((prev) => ({
...prev,
transactionType: val ? ((val as OptionType).value as string) : '',
transactionType: val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
}));
};
const bankChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedBank(val as OptionType);
setSelectedBank(val);
setPendingFilters((prev) => ({
...prev,
bankId: val ? ((val as OptionType).value as string) : '',
bankId: val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
}));
};
const partyTypeChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedPartyType(val as OptionType);
const customerIdChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedCustomerId(val);
setPendingFilters((prev) => ({
...prev,
partyType: val ? ((val as OptionType).value as string) : '',
customerId: val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
}));
};
const supplierIdChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedSupplierId(val);
setPendingFilters((prev) => ({
...prev,
supplierId: val
? Array.isArray(val)
? val.map((item) => item.value).join(',')
: (val.value as string)
: '',
}));
};
const sortByChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -279,7 +298,8 @@ const FinanceTable = () => {
updateFilter('search', pendingFilters.search);
updateFilter('transactionType', pendingFilters.transactionType);
updateFilter('bankId', pendingFilters.bankId);
updateFilter('partyType', pendingFilters.partyType);
updateFilter('customerId', pendingFilters.customerId);
updateFilter('supplierId', pendingFilters.supplierId);
updateFilter('sortBy', pendingFilters.sortBy);
updateFilter('startDate', pendingFilters.startDate);
updateFilter('endDate', pendingFilters.endDate);
@@ -287,14 +307,16 @@ const FinanceTable = () => {
const resetFilterHandler = () => {
setSelectedTransactionType(null);
setSelectedBank(null);
setSelectedPartyType(null);
setSelectedCustomerId(null);
setSelectedSupplierId(null);
setSelectedSortBy(null);
const emptyFilters = {
search: '',
transactionType: '',
bankId: '',
partyType: '',
customerId: '',
supplierId: '',
sortBy: '',
startDate: '',
endDate: '',
@@ -304,7 +326,8 @@ const FinanceTable = () => {
updateFilter('search', '');
updateFilter('transactionType', '');
updateFilter('bankId', '');
updateFilter('partyType', '');
updateFilter('customerId', '');
updateFilter('supplierId', '');
updateFilter('sortBy', '');
updateFilter('startDate', '');
updateFilter('endDate', '');
@@ -477,27 +500,34 @@ const FinanceTable = () => {
>
<div className='grid grid-cols-4 gap-6'>
<SelectInput
options={transactionTypeOptions}
label='Tipe Transaksi'
options={FINANCE_TRANSACTION_TYPE_OPTIONS}
label='Jenis Transaksi'
value={selectedTransactionType}
onChange={transactionTypeChangeHandler}
isClearable
isMulti
/>
<SelectInput
options={partyTypeOptions}
label={
selectedTransactionType
? selectedTransactionType.value === 'CUSTOMER'
? 'Pelanggan'
: 'Supplier'
: 'Pihak'
}
value={selectedPartyType}
onChange={partyTypeChangeHandler}
onInputChange={partyTypeInputValue}
onMenuScrollToBottom={partyTypeLoadMore}
isLoading={partyTypeIsLoadingOptions}
options={customerOptions}
label={'Customer'}
value={selectedCustomerId}
onChange={customerIdChangeHandler}
onInputChange={customerInputValue}
onMenuScrollToBottom={customerLoadMore}
isLoading={customerIsLoadingOptions}
isClearable
isMulti
/>
<SelectInput
options={supplierOptions}
label={'Supplier'}
value={selectedSupplierId}
onChange={supplierIdChangeHandler}
onInputChange={supplierInputValue}
onMenuScrollToBottom={supplierLoadMore}
isLoading={supplierIsLoadingOptions}
isClearable
isMulti
/>
<SelectInput
options={
@@ -522,13 +552,7 @@ const FinanceTable = () => {
onInputChange={bankInputValue}
onMenuScrollToBottom={bankLoadMore}
isClearable
/>
<DebouncedTextInput
name='search'
label='Cari'
placeholder='Cari'
value={pendingFilters.search}
onChange={searchChangeHandler}
isMulti
/>
<SelectInput
options={sortByOptions}
@@ -549,6 +573,13 @@ const FinanceTable = () => {
value={pendingFilters.endDate}
onChange={endDateChangeHandler}
/>
<DebouncedTextInput
name='search'
label='Cari'
placeholder='Cari'
value={pendingFilters.search}
onChange={searchChangeHandler}
/>
</div>
</Card>
<Table<Finance>
@@ -245,7 +245,11 @@ const FormFinanceAddInitialBalance = ({
}
required
isClearable
isDisabled={!formik.values.party_type_option?.value}
isDisabled={
!formik.values.party_type_option?.value ||
(type === 'edit' &&
formik.values.party_type_option?.value == 'SUPPLIER')
}
/>
<SelectInput
label='Bank'
@@ -323,6 +327,7 @@ const FormFinanceAddInitialBalance = ({
}
required
isClearable
isDisabled={type == 'edit'}
/>
<NumberInput
label='Nominal'
@@ -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',
@@ -89,7 +89,6 @@ const MarketingDetail = ({
deleteModal.closeModal();
router.push('/marketing');
toast.success(res?.message as string);
refresh?.();
setIsLoading(false);
};
@@ -361,6 +361,8 @@ const MarketingForm = ({
},
});
const memoSalesOrder = formik.values.sales_order;
// ================== FORM REPEATER HANDLER ==================
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
setIsLoading(true);
@@ -471,13 +473,25 @@ const MarketingForm = ({
}, [deleteModal]);
// ================== SALES ORDER HANDLER ==================
const handleDeleteSO = useCallback((id: number) => {
const currentProducts = formik.values.sales_order;
formik.setFieldValue(
'sales_order',
currentProducts.filter((p) => p.id != id)
);
}, []);
const handleDeleteSO = useCallback(
(id: number) => {
const currentProducts = formik.values.sales_order;
formik.setFieldValue(
'sales_order',
currentProducts.filter((p) => p.id != id)
);
},
[memoSalesOrder]
);
const handleEditSO = useCallback(
(id: number) => {
const currentProducts = formik.values.sales_order;
const selectedProduct = currentProducts.find((p) => p.id == id);
setSelectedMarketingProduct(selectedProduct ?? null);
addSOModal.openModal();
},
[memoSalesOrder]
);
const handleBulkDeleteSO = useCallback(() => {
const currentProducts = formik.values.sales_order;
formik.setFieldValue(
@@ -487,13 +501,13 @@ const MarketingForm = ({
)
);
setRowSOSelection({});
}, [selectedRowSOIds]);
}, [selectedRowSOIds, memoSalesOrder]);
const handleAddSOClick = useCallback(() => {
setSelectedMarketingProduct(null);
addSOModal.openModal();
}, [addSOModal]);
const handleAddSubmitSO = useCallback(
async (values: SalesOrderProductFormValues) => {
async (values: SalesOrderProductFormValues, id?: number) => {
const currentProducts = formik.values.sales_order;
const newValues = {
@@ -501,18 +515,12 @@ const MarketingForm = ({
id: values.id ?? Date.now(),
};
const existingIndex = currentProducts.findIndex(
(item) =>
item.kandang_id === newValues.kandang_id &&
item.product_warehouse_id === newValues.product_warehouse_id
);
let updatedProducts = [];
if (existingIndex !== -1) {
if (id) {
// Overwrite
updatedProducts = currentProducts.map((item, index) =>
index === existingIndex ? newValues : item
updatedProducts = currentProducts.map((item) =>
item.id === id ? newValues : item
);
} else {
// Add new item
@@ -523,7 +531,7 @@ const MarketingForm = ({
addSOModal.closeModal();
},
[addSOModal]
[addSOModal, memoSalesOrder]
);
// ================== DELIVERY ORDER HANDLER ==================
@@ -568,8 +576,30 @@ const MarketingForm = ({
},
[addDOModal]
);
const memoSalesOrder = formik.values.sales_order;
const handleDeleteDO = useCallback(
async (id: number) => {
setDeliveryOrderValues((prev) =>
prev.map((product) =>
product.id === id
? {
...product,
...{
unit_price: '',
total_weight: '',
qty: '',
avg_weight: '',
total_price: '',
delivery_date: '',
},
}
: product
)
);
addDOModal.closeModal();
setSelectedDeliveryProduct(null);
},
[addDOModal]
);
useEffect(() => {
formik.setFieldValue('delivery_order', deliveryOrderValues);
@@ -654,6 +684,7 @@ const MarketingForm = ({
setRowSelection={setRowSOSelection}
selectedRowIds={selectedRowSOIds}
onDelete={handleDeleteSO}
onEdit={handleEditSO}
onBulkDelete={handleBulkDeleteSO}
onAddProductClick={handleAddSOClick}
/>
@@ -673,6 +704,7 @@ const MarketingForm = ({
formType={formType}
data={deliveryOrderValues}
onEdit={handleEditDO}
onDelete={handleDeleteDO}
onAddProductClick={handleAddDOClick}
/>
</Card>
@@ -106,6 +106,7 @@ const DeliveryOrderProductForm = ({
await onUpdateForm?.(values.marketing_product_id as number, values);
}
handleResetForm();
setSelectedProduct(null);
},
});
@@ -124,7 +125,7 @@ const DeliveryOrderProductForm = ({
marketing_product: undefined,
},
});
setSelectedProduct(null);
// setSelectedProduct(null);
};
const handleBlurField = (field: string) => {
@@ -18,6 +18,7 @@ type SalesOrderProductSchemaType = {
avg_weight: string | number | undefined;
total_price: string | number | undefined;
vehicle_number?: string | undefined;
uom?: string | null | undefined;
};
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
@@ -57,6 +58,7 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
total_price: Yup.number()
.min(1, 'Total Penjualan wajib diisi!')
.required('Total Penjualan wajib diisi!'),
uom: Yup.string().nullable().optional().notRequired(),
});
export type SalesOrderProductFormValues = Yup.InferType<
@@ -39,7 +39,10 @@ const SalesOrderProductForm = ({
initialValues?: SalesOrderProductFormValues;
exisitingValues?: SalesOrderProductFormValues[];
modalRef?: RefObject<HTMLDialogElement | null>;
onSubmitForm?: (value: SalesOrderProductFormValues) => Promise<void>;
onSubmitForm?: (
value: SalesOrderProductFormValues,
id?: number
) => Promise<void>;
}) => {
const [formErrorMessage, setFormErrorMessage] = useState('');
const [currentInput, setCurrentInput] = useState<string>('');
@@ -61,21 +64,22 @@ const SalesOrderProductForm = ({
const formik = useFormik<SalesOrderProductFormValues>({
enableReinitialize: true,
initialValues: {
vehicle_number: initialValues?.vehicle_number || undefined,
vehicle_number: initialValues?.vehicle_number || '',
kandang_id: initialValues?.kandang_id || undefined,
kandang: initialValues?.kandang || undefined,
product_warehouse: initialValues?.product_warehouse || undefined,
kandang: initialValues?.kandang || null,
product_warehouse: initialValues?.product_warehouse || null,
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
unit_price: initialValues?.unit_price || undefined,
total_weight: initialValues?.total_weight || undefined,
qty: initialValues?.qty || undefined,
avg_weight: initialValues?.avg_weight || undefined,
total_price: initialValues?.total_price || undefined,
unit_price: initialValues?.unit_price || '',
total_weight: initialValues?.total_weight || '',
qty: initialValues?.qty || '',
avg_weight: initialValues?.avg_weight || '',
total_price: initialValues?.total_price || '',
uom: initialValues?.uom || '',
},
validationSchema: SalesOrderProductSchema,
onSubmit: async (values) => {
setFormErrorMessage('');
onSubmitForm?.(values);
onSubmitForm?.(values, initialValues?.id);
handleResetForm();
},
validateOnBlur: true,
@@ -220,7 +224,19 @@ const SalesOrderProductForm = ({
};
// ===== Formik Error List =====
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
formik,
{
onBeforeSubmit(e) {
e.preventDefault();
handleBlurField(currentInput);
formik.setFieldValue(
'uom',
isResponseSuccess(productData) ? productData?.data?.uom.name : ''
);
},
}
);
return (
<>
@@ -401,7 +417,9 @@ const SalesOrderProductForm = ({
/>
</div>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
<div className='mt-4'>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
</div>
<div className='flex flex-row justify-end gap-3 mt-4'>
<Button type='reset' color='warning' onClick={handleResetForm}>
@@ -16,6 +16,7 @@ type DeliveryOrderProductTableProps = {
data: DeliveryOrderProductFormValues[];
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
onEdit: (id: number) => void;
onDelete: (id: number) => void;
onAddProductClick: () => void;
};
@@ -23,10 +24,13 @@ const DeliveryOrderProductTable = ({
data,
formType,
onEdit,
onDelete,
onAddProductClick,
}: DeliveryOrderProductTableProps) => {
const onEditRef = useRef(onEdit);
onEditRef.current = onEdit;
const onDeleteRef = useRef(onDelete);
onDeleteRef.current = onDelete;
const canAddData = data.filter((item) => !Boolean(item.qty));
@@ -144,16 +148,29 @@ const DeliveryOrderProductTable = ({
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
<>
{props.row.original.qty && (
<Button
color='warning'
className='px-2 py-1 text-sm'
onClick={() =>
onEditRef.current(props.row.original.id as number)
}
type='button'
>
<Icon icon='mdi:edit' width={16} height={16} /> Edit
</Button>
<>
<Button
color='warning'
className='px-2 py-1 text-sm'
onClick={() =>
onEditRef.current(props.row.original.id as number)
}
type='button'
>
<Icon icon='mdi:edit' width={16} height={16} /> Edit
</Button>
<Button
color='error'
className='px-2 py-1 text-sm'
onClick={() =>
onDeleteRef.current(props.row.original.id as number)
}
type='button'
disabled={!!props.row.original.do_number}
>
<Icon icon='mdi:delete' width={16} height={16} /> Hapus
</Button>
</>
)}
{!props.row.original.qty && '-'}
</>
@@ -23,6 +23,7 @@ type SalesOrderProductTableProps = {
>;
selectedRowIds: number[];
onDelete: (id: number) => void;
onEdit: (id: number) => void;
onBulkDelete: () => void;
onAddProductClick: () => void;
};
@@ -34,11 +35,14 @@ const SalesOrderProductTable = ({
setRowSelection,
selectedRowIds,
onDelete,
onEdit,
onBulkDelete,
onAddProductClick,
}: SalesOrderProductTableProps) => {
const onDeleteRef = useRef(onDelete);
onDeleteRef.current = onDelete;
const onEditRef = useRef(onEdit);
onEditRef.current = onEdit;
const columns = useMemo(
() => [
@@ -92,17 +96,26 @@ const SalesOrderProductTable = ({
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatNumber(parseFloat(row.total_weight as string)),
formatNumber(parseFloat(row.total_weight as string), undefined, 0, 5),
header: 'Total Bobot (Kg)',
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatNumber(parseFloat(row.qty as string)),
header: 'Kuantitas',
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) =>
formatNumber(
parseFloat(row.original.qty as string),
undefined,
0,
5
) +
' ' +
(row.original.uom ?? ''),
},
{
accessorFn: (row: SalesOrderProductFormValues) =>
formatNumber(parseFloat(row.avg_weight as string)),
formatNumber(parseFloat(row.avg_weight as string), undefined, 0, 5),
header: 'Avg. Bobot (Kg)',
},
{
@@ -116,6 +129,14 @@ const SalesOrderProductTable = ({
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
) => (
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
<Button
color='warning'
className='p-1'
onClick={() => onEditRef.current(props.row.original.id as number)}
type='button'
>
<Icon icon='mdi:pencil' width={16} height={16} /> Edit
</Button>
<Button
color='error'
className='p-1'
@@ -124,7 +145,7 @@ const SalesOrderProductTable = ({
}
type='button'
>
<Icon icon='mdi:trash' width={16} height={16} />
<Icon icon='mdi:trash' width={16} height={16} /> Hapus
</Button>
</div>
),
@@ -7,6 +7,7 @@ import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
import { format } from 'path';
import { date } from 'yup';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
import toast from 'react-hot-toast';
interface DeliveryOrderExportProps {
data?: Marketing;
@@ -23,7 +24,7 @@ const DeliveryOrderExport = ({
const handleDownloadPDF = async () => {
if (!salesData) {
alert('No sales order data available');
toast.error('No sales order data available');
return;
}
setIsGeneratingPDF(true);
@@ -40,8 +41,7 @@ const DeliveryOrderExport = ({
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error generating PDF:', error);
alert('Failed to generate PDF. Please try again.');
toast.error('Failed to generate PDF. Please try again.');
} finally {
setIsGeneratingPDF(false);
}
@@ -5,6 +5,7 @@ import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
import { useMemo, useState } from 'react';
import { formatDate, formatNumber } from '@/lib/helper';
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
import toast from 'react-hot-toast';
interface SalesOrderExportProps {
data?: Marketing;
@@ -17,7 +18,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
const handleDownloadPDF = async () => {
if (!salesData) {
alert('No sales order data available');
toast.error('No sales order data available');
return;
}
setIsGeneratingPDF(true);
@@ -32,8 +33,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error generating PDF:', error);
alert('Failed to generate PDF. Please try again.');
toast.error('Failed to generate PDF. Please try again.');
} finally {
setIsGeneratingPDF(false);
}
@@ -367,7 +367,7 @@ const ProductionStandardForm = ({
accessorFn: (row) =>
row.production_standard_details?.target_hen_house_production,
cell: ({ row }) =>
`${row.original.production_standard_details?.target_hen_house_production} pc`,
`${row.original.production_standard_details?.target_hen_house_production} btr`,
enableSorting: false,
},
{
@@ -383,7 +383,7 @@ const ProductionStandardForm = ({
accessorFn: (row) =>
row.production_standard_details?.target_egg_mass,
cell: ({ row }) =>
`${row.original.production_standard_details?.target_egg_mass} g`,
`${row.original.production_standard_details?.target_egg_mass} kg`,
enableSorting: false,
},
{
@@ -958,7 +958,7 @@ const ProductionStandardForm = ({
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
bottomLabel='Butir (pc)'
bottomLabel='Butir (btr)'
errorMessage={getProductionDetailsError(
repeaterFormik.errors
.production_standard_details,
@@ -1015,7 +1015,7 @@ const ProductionStandardForm = ({
name='production_standard_details.target_egg_mass'
label='Egg Mass'
placeholder='1'
bottomLabel='Gram (g)'
bottomLabel='Kg (kg)'
value={
repeaterFormik.values
.production_standard_details?.target_egg_mass
@@ -1176,7 +1176,7 @@ const ProductionStandardForm = ({
}
onChange={repeaterFormik.handleChange}
onBlur={repeaterFormik.handleBlur}
bottomLabel='Gram/Ekor (g)'
bottomLabel='Gram (g)'
endAdornment
errorMessage={
repeaterFormik.errors
@@ -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,6 @@ 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'),
@@ -1011,21 +1013,6 @@ const RecordingTable = () => {
approvalHistoryModal.openModal();
};
const getStatusText = (action: string) => {
switch (action) {
case 'APPROVED':
return 'Disetujui';
case 'REJECTED':
return 'Ditolak';
case 'CREATED':
return 'Dibuat';
case 'UPDATED':
return 'Diperbarui';
default:
return action;
}
};
return (
<Badge
variant='soft'
@@ -1036,7 +1023,7 @@ const RecordingTable = () => {
}}
onClick={openApprovalHistory}
>
{getStatusText(approval.action)}
{approval.step_name || approval.action}
</Badge>
);
},
@@ -33,16 +33,16 @@ type RecordingGrowingFormSchemaType = {
qty: number | string;
}[];
depletions: {
product_warehouse_id: number;
qty: number | string;
product_warehouse_id?: number;
qty?: number | string;
}[];
};
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
eggs: {
product_warehouse_id: number;
qty: number | string;
weight: number | string;
product_warehouse_id?: number;
qty?: number | string;
weight?: number | string;
}[];
};
@@ -52,14 +52,14 @@ export type StockSchema = {
};
export type DepletionSchema = {
product_warehouse_id: number;
qty: number | string;
product_warehouse_id?: number;
qty?: number | string;
};
export type EggSchema = {
product_warehouse_id: number;
qty: number | string;
weight: number | string;
product_warehouse_id?: number;
qty?: number | string;
weight?: number | string;
};
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
@@ -75,28 +75,19 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
product_warehouse_id: Yup.number()
.required('Produk depletions wajib diisi!')
.min(1, 'Produk depletions wajib diisi!')
.typeError('Produk depletions harus berupa angka!'),
.optional()
.typeError('Depletions harus berupa angka!'),
qty: Yup.number()
.required('Jumlah depletions wajib diisi!')
.min(1, 'Jumlah depletions minimal 1!')
.optional()
.typeError('Jumlah depletions harus berupa angka!'),
});
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
product_warehouse_id: Yup.number()
.required('Kondisi telur wajib diisi!')
.min(1, 'Kondisi telur wajib diisi!')
.optional()
.typeError('Kondisi telur harus berupa angka!'),
qty: Yup.number()
.required('Jumlah telur wajib diisi!')
.min(1, 'Jumlah telur tidak boleh 0!')
.typeError('Jumlah telur harus berupa angka!'),
weight: Yup.number()
.required('Berat telur wajib diisi!')
.min(1, 'Berat telur minimal 1 gram!')
.typeError('Berat telur harus berupa angka!'),
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
});
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
@@ -163,18 +154,12 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
.of(StockObjectSchema)
.min(1, 'Minimal harus ada 1 data stok!')
.required('Data stok wajib diisi!'),
depletions: Yup.array()
.of(DepletionObjectSchema)
.min(1, 'Minimal harus ada 1 data depletions!')
.required('Data depletions wajib diisi!'),
depletions: Yup.array().of(DepletionObjectSchema).default([]),
});
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
RecordingGrowingFormSchema.shape({
eggs: Yup.array()
.of(EggObjectSchema)
.min(1, 'Minimal harus ada 1 data telur!')
.required('Data telur wajib diisi!'),
eggs: Yup.array().of(EggObjectSchema).default([]),
});
export const UpdateRecordingGrowingFormSchema =
@@ -79,6 +79,7 @@ import {
GROWING_RECORDING_APPROVAL_LINE,
LAYING_RECORDING_APPROVAL_LINE,
} from '@/config/approval-line';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
interface RecordingFormProps {
type?: 'add' | 'edit' | 'detail';
@@ -227,7 +228,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const [, setApprovalNotes] = useState('');
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
useState('');
const [formErrorList, setFormErrorList] = useState<string[]>([]);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [, setNewRecordingData] = useState<Recording | null>(null);
const [nextDayRecording, setNextDayRecording] =
@@ -309,6 +309,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
// ===== PAYLOAD CREATION HELPERS =====
const createGrowingPayload = useCallback(
(values: RecordingGrowingFormValues) => {
const depletions = values.depletions
?.filter((d) => d.product_warehouse_id && d.qty)
.map((depletion) => ({
product_warehouse_id: depletion.product_warehouse_id!,
qty: Number(depletion.qty) || 0,
}));
return {
project_flock_kandang_id: values.project_flock_kandang_id,
record_date: values.record_date,
@@ -316,10 +323,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
product_warehouse_id: stock.product_warehouse_id,
qty: Number(stock.qty) || 0,
})),
depletions: (values.depletions ?? []).map((depletion) => ({
product_warehouse_id: depletion.product_warehouse_id,
qty: Number(depletion.qty) || 0,
})),
...(depletions && depletions.length > 0 && { depletions }),
};
},
[]
@@ -327,25 +331,33 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
const createLayingPayload = useCallback(
(values: RecordingLayingFormValues) => {
return {
project_flock_kandang_id: values.project_flock_kandang_id,
record_date: values.record_date,
stocks: (values.stocks ?? []).map((stock) => ({
product_warehouse_id: stock.product_warehouse_id,
qty: Number(stock.qty) || 0,
})),
depletions: (values.depletions ?? []).map((depletion) => ({
product_warehouse_id: depletion.product_warehouse_id,
const depletions = values.depletions
?.filter((d) => d.product_warehouse_id && d.qty)
.map((depletion) => ({
product_warehouse_id: depletion.product_warehouse_id!,
qty: Number(depletion.qty) || 0,
})),
eggs: (values.eggs ?? []).map((egg) => ({
product_warehouse_id: egg.product_warehouse_id,
}));
const eggs = values.eggs
?.filter((e) => e.product_warehouse_id && e.qty && e.weight)
.map((egg) => ({
product_warehouse_id: egg.product_warehouse_id!,
qty: Number(egg.qty) || 0,
weight:
typeof egg.weight === 'number'
? egg.weight
: parseFloat(String(egg.weight)) || 0,
}));
return {
project_flock_kandang_id: values.project_flock_kandang_id,
record_date: values.record_date,
stocks: values.stocks.map((stock) => ({
product_warehouse_id: stock.product_warehouse_id,
qty: Number(stock.qty) || 0,
})),
...(depletions && depletions.length > 0 && { depletions }),
...(eggs && eggs.length > 0 && { eggs }),
};
},
[]
@@ -905,10 +917,58 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
baseValues = getRecordingGrowingFormInitialValues(initialValues);
}
if (type === 'add') {
baseValues.location = selectedLocation
? {
value: Number(selectedLocation.value),
label: selectedLocation.label,
}
: null;
baseValues.location_id = selectedLocation
? Number(selectedLocation.value)
: 0;
baseValues.project_flock = selectedProjectFlock
? {
value: Number(selectedProjectFlock.value),
label: selectedProjectFlock.label,
}
: null;
baseValues.project_flock_id = selectedProjectFlock
? Number(selectedProjectFlock.value)
: 0;
baseValues.kandang = selectedKandang
? {
value: Number(selectedKandang.value),
label: selectedKandang.label,
}
: null;
baseValues.kandang_id = selectedKandang
? Number(selectedKandang.value)
: 0;
}
if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) {
baseValues.project_flock_kandang = {
value: projectFlockKandangDetail.project_flock.id,
label: projectFlockKandangDetail.project_flock.flock_name || '',
baseValues = {
...baseValues,
project_flock_kandang: {
value: projectFlockKandangDetail.project_flock?.id,
label: projectFlockKandangDetail.project_flock?.flock_name || '',
},
project_flock: {
value: projectFlockKandangDetail.project_flock?.id,
label: projectFlockKandangDetail.project_flock?.flock_name || '',
},
project_flock_id: projectFlockKandangDetail.project_flock?.id,
location: {
value: projectFlockKandangDetail.project_flock?.location?.id,
label: projectFlockKandangDetail.project_flock?.location?.name || '',
},
location_id: projectFlockKandangDetail.project_flock?.location?.id,
kandang: {
value: projectFlockKandangDetail.kandang?.id,
label: projectFlockKandangDetail.kandang?.name || '',
},
kandang_id: projectFlockKandangDetail.kandang?.id,
};
}
@@ -995,22 +1055,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
},
});
const handleValidateForm = async () => {
const errors = await formik.validateForm();
if (Object.keys(errors).length > 0) {
const errorMessages = getUniqueFormikErrors(errors);
setFormErrorList(errorMessages);
return;
}
};
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
handleValidateForm();
formik.handleSubmit(e);
};
// ===== HELPER FUNCTIONS =====
const getAvailableStock = useCallback(
(productWarehouseId: number) => {
@@ -1266,6 +1310,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
[formik, duplicateErrorShown]
);
const { formErrorList, handleFormSubmit, close } = useFormikErrorList(formik);
useEffect(() => {
if (projectFlockKandangLookup?.project_flock_kandang_id) {
const projectFlockKandangId =
@@ -1655,10 +1701,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
{/* Error List Alert */}
{formErrorList.length > 0 && (
<AlertErrorList
formErrorList={formErrorList}
onClose={() => setFormErrorList([])}
/>
<AlertErrorList formErrorList={formErrorList} onClose={close} />
)}
{/* Basic Info Card */}
@@ -2520,24 +2563,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/>
</th>
)}
<th>
Kondisi
<span
className='tooltip tooltip-error tooltip-bottom '
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Jumlah
<span
className='tooltip tooltip-error tooltip-bottom '
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>Kondisi</th>
<th>Jumlah</th>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>Action</th>
)}
@@ -2615,7 +2642,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td>
<td>
<NumberInput
required
name={`depletions.${idx}.qty`}
value={depletion.qty ?? ''}
onChange={handleDepletionQtyChangeWrapper(idx)}
@@ -2731,33 +2757,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
/>
</th>
)}
<th>
Kondisi Telur
<span
className='tooltip tooltip-error tooltip-bottom '
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Jumlah
<span
className='tooltip tooltip-error tooltip-bottom '
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>
Berat (gram)
<span
className='tooltip tooltip-error tooltip-bottom '
data-tip='required'
>
<span className='text-error'>*</span>
</span>
</th>
<th>Kondisi Telur</th>
<th>Jumlah</th>
<th>Berat (gram)</th>
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
<th>Action</th>
)}
@@ -2792,7 +2794,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
)}
<td>
<SelectInput
required
value={
eggProducts.find(
(product) =>
@@ -2835,7 +2836,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td>
<td>
<NumberInput
required
name={`eggs.${idx}.qty`}
value={egg.qty ?? ''}
onChange={handleEggQtyChangeWrapper(idx)}
@@ -2860,7 +2860,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
</td>
<td>
<NumberInput
required
name={`eggs.${idx}.weight`}
value={egg.weight ?? ''}
onChange={handleEggWeightChangeWrapper(idx)}
@@ -540,31 +540,6 @@ const PurchaseOrderDetail = ({
accessorKey: 'travel_number',
cell: (props) => props.row.original.travel_number || '-',
},
{
header: 'Dokumen Surat Jalan',
accessorKey: 'travel_document_path',
cell: (props) => {
const documentPath = props.row.original.travel_document_path;
return documentPath ? (
<Button
color='primary'
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm'
href={documentPath}
target='_blank'
rel='noopener noreferrer'
>
<Icon
icon='material-symbols:file-open-outline'
width={16}
height={16}
/>
Lihat Dokumen
</Button>
) : (
'-'
);
},
},
{
header: 'No. Armada Pengangkut',
accessorKey: 'vehicle_number',
@@ -588,7 +563,10 @@ const PurchaseOrderDetail = ({
{
header: 'Transport /Item',
accessorKey: 'transport_per_item',
cell: (props) => formatCurrency(props.getValue() as number),
cell: (props) => {
const value = props.row.original.transport_per_item;
return value ? formatCurrency(value) : formatCurrency(0);
},
},
];
@@ -723,8 +701,8 @@ const PurchaseOrderDetail = ({
</span>
<span className='text-gray-900 ml-3 break-all'>
:{' '}
{purchaseData.items?.[0]?.warehouse?.type === 'LOKASI' &&
purchaseData.items?.[0]?.warehouse?.location?.name
{purchaseData.items?.[0]?.warehouse &&
'location' in purchaseData.items[0].warehouse
? purchaseData.items[0].warehouse.location.name
: '-'}
</span>
@@ -905,11 +883,29 @@ const PurchaseOrderDetail = ({
Informasi Penerimaan Barang
</h3>
{canShowPenerimaanBarang && (
<RowDropdownOptions isLast2Rows>
<PenerimaanBarangDropdown
onEdit={penerimaanBarangModal.openModal}
/>
</RowDropdownOptions>
<div className='flex items-center gap-2'>
{goodsReceiptItems[0]?.travel_document_path && (
<Button
color='primary'
className='w-fit min-w-32 flex items-center justify-start gap-1 p-1.5 text-sm'
href={goodsReceiptItems[0].travel_document_path}
target='_blank'
rel='noopener noreferrer'
>
<Icon
icon='material-symbols:file-open-outline'
width={16}
height={16}
/>
Lihat Dokumen
</Button>
)}
<RowDropdownOptions isLast2Rows>
<PenerimaanBarangDropdown
onEdit={penerimaanBarangModal.openModal}
/>
</RowDropdownOptions>
</div>
)}
</div>
<div className='overflow-x-auto'>
@@ -324,12 +324,14 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
PT LUMBUNG TELUR INDONESIA
</Text>
<Text>
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
{purchaseData?.items?.[0]?.warehouse &&
'location' in purchaseData.items[0].warehouse
? purchaseData.items[0].warehouse.location.name
: '-'}
</Text>
<Text>
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
{purchaseData?.items?.[0]?.warehouse &&
'location' in purchaseData.items[0].warehouse
? purchaseData.items[0].warehouse.location.address
: '-'}
</Text>
@@ -434,7 +436,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
</View>
<View style={pdfStyles.tableCell}>
<Text>
{item.warehouse?.type === 'LOKASI'
{item.warehouse && 'location' in item.warehouse
? item.warehouse.location.address
: '-'}
</Text>
@@ -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>
@@ -10,7 +10,7 @@ import DateInput from '@/components/input/DateInput';
import NumberInput from '@/components/input/NumberInput';
import { AreaApi } from '@/services/api/master-data';
import { LocationApi } from '@/services/api/master-data';
import { KandangApi } from '@/services/api/master-data';
import { ProjectFlockKandangApi } from '@/services/api/production';
import { SaleReportApi } from '@/services/api/report/marketing-sale';
import Table from '@/components/Table';
import { ColumnDef, Row, flexRender } from '@tanstack/react-table';
@@ -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: {
@@ -77,7 +80,12 @@ const HppPerKandangTab = () => {
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
loadMore: loadMoreKandangs,
} = useSelect(KandangApi.basePath, 'id', 'name', 'search');
} = useSelect(
ProjectFlockKandangApi.basePath,
'id',
'name_with_period',
'search'
);
const showUnrecordedOptions: OptionType[] = [
{ value: 'false', label: 'Sembunyikan' },
@@ -127,8 +135,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 +148,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 +349,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 +453,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`;
@@ -488,8 +562,8 @@ const HppPerKandangTab = () => {
header: 'Kandang',
accessorKey: 'kandang.name',
cell: (props) => {
const kandang = props.row.original.kandang;
return kandang?.name || '-';
const row = props.row.original;
return row.name_with_periode || row.kandang?.name || '-';
},
footer: () => <div className='font-semibold text-gray-900'>ALL</div>,
},
@@ -741,6 +815,8 @@ const HppPerKandangTab = () => {
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas}
isLoading={isLoadingAreas}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isClearable
/>
<SelectInput
@@ -757,6 +833,8 @@ const HppPerKandangTab = () => {
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isLoading={isLoadingLocations}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isClearable
/>
<SelectInput
@@ -773,6 +851,8 @@ const HppPerKandangTab = () => {
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingKandangs}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isClearable
/>
</div>
@@ -792,6 +872,8 @@ const HppPerKandangTab = () => {
placeholder='Masukkan bobot maximum'
value={tableFilterState.weight_max}
onChange={weightMaxChangeHandler}
isError={!!weightMaxError}
errorMessage={weightMaxError}
/>
</div>
<DateInput
@@ -818,7 +900,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>
+1 -17
View File
@@ -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;
+8
View File
@@ -389,6 +389,14 @@ export const FINANCE_INITIAL_BALANCE_TYPE_OPTIONS = [
{ label: 'Saldo Awal Negatif', value: 'NEGATIVE' },
];
export const FINANCE_TRANSACTION_TYPE_OPTIONS = [
{ label: 'Pembelian', value: 'PEMBELIAN' },
{ label: 'Penjualan', value: 'PENJUALAN' },
{ label: 'Biaya', value: 'BIAYA' },
{ label: 'Saldo Awal', value: 'SALDO_AWAL' },
{ label: 'Injection', value: 'INJECTION' },
];
export const FINANCE_TRANSACTION_STATUS = ['PENJUALAN', 'PEMBELIAN', 'BIAYA'];
export const FINANCE_INITIAL_BALANCE_STATUS = ['SALDO_AWAL'];
+18
View File
@@ -11,6 +11,8 @@ import {
ClosingSapronakCalculation,
ClosingProductionData,
ClosingHppExpedition,
ClosingIncomingSapronakSummary,
ClosingOutgoingSapronakSummary,
} from '@/types/api/closing';
import { BaseApiResponse } from '@/types/api/api-general';
import { httpClient, httpClientFetcher } from '@/services/http/client';
@@ -62,6 +64,14 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
);
}
async getAllIncomingSapronakSummaryFetcher(
endpoint: string
): Promise<BaseApiResponse<ClosingIncomingSapronakSummary[]>> {
return await httpClientFetcher<
BaseApiResponse<ClosingIncomingSapronakSummary[]>
>(endpoint);
}
async getAllOutgoingSapronakFetcher(
endpoint: string
): Promise<BaseApiResponse<ClosingOutgoingSapronak[]>> {
@@ -70,6 +80,14 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
);
}
async getAllOutgoingSapronakSummaryFetcher(
endpoint: string
): Promise<BaseApiResponse<ClosingOutgoingSapronakSummary[]>> {
return await httpClientFetcher<
BaseApiResponse<ClosingOutgoingSapronakSummary[]>
>(endpoint);
}
async getGeneralInfo(
id: number
): Promise<BaseApiResponse<ClosingGeneralInformation> | undefined> {
+3 -6
View File
@@ -48,8 +48,7 @@ export class SalesOrderService extends BaseApiService<
},
});
} catch (error) {
console.error('Error approve marketing:', error);
return undefined;
throw error;
}
}
@@ -72,8 +71,7 @@ export class SalesOrderService extends BaseApiService<
},
});
} catch (error) {
console.error('Error bulk approve marketing:', error);
return undefined;
throw error;
}
}
@@ -95,8 +93,7 @@ export class SalesOrderService extends BaseApiService<
},
});
} catch (error) {
console.error('Error delivery marketing:', error);
return undefined;
throw error;
}
}
}
+1 -2
View File
@@ -35,8 +35,7 @@ export class ChickinService extends BaseApiService<
},
});
} catch (error) {
console.error('Error approve chickin:', error);
return undefined;
throw error;
}
}
}
+2 -2
View File
@@ -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,
@@ -25,8 +25,6 @@ export const createDashboardFilterSlice: StateCreator<
setFilterValues: (values) => set({ filterValues: values }),
resetFilterValues: () => {
alert('reset filter values');
return set({
filterValues: {
startDate: '',
+9
View File
@@ -11,6 +11,7 @@ import { Product } from '@type/api/master-data/product';
import { Customer } from '@type/api/master-data/customer';
import { BaseMetadata } from '@/types/api/api-general';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { BaseUom } from '@/types/api/master-data/uom';
export type BaseSales = {
id: number;
@@ -104,8 +105,16 @@ export type ClosingIncomingSapronak = {
notes: string;
};
export type ClosingIncomingSapronakSummary = {
category: string;
total_qty: number;
uom: BaseUom;
};
export type ClosingOutgoingSapronak = ClosingIncomingSapronak;
export type ClosingOutgoingSapronakSummary = ClosingIncomingSapronakSummary;
export type ClosingProductionData = {
purchase: {
initial_population: number;
+2 -2
View File
@@ -34,7 +34,7 @@ export type BaseExpense = {
nonstock_id: number;
qty: number;
price: number;
note?: string;
notes?: string;
nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>;
created_at: string;
}[];
@@ -43,7 +43,7 @@ export type BaseExpense = {
expense_nonstock_id: number;
qty: number;
price: number;
note?: string;
notes?: string;
nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>;
created_at: string;
}[];
+1
View File
@@ -10,6 +10,7 @@ export type BaseProjectFlockKandang = {
kandang_id: number;
kandang: Kandang;
project_flock: ProjectFlock;
name_with_period?: string;
approval: BaseApproval;
chickins?: Chickin[];
available_qtys?: AvailableQty[];
+9 -5
View File
@@ -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[];
@@ -107,15 +111,15 @@ export type CreateGrowingRecordingPayload = {
qty: number;
}[];
depletions?: {
product_warehouse_id: number;
qty: number;
product_warehouse_id?: number;
qty?: number;
}[];
};
export type CreateEggPayload = {
product_warehouse_id: number;
qty: number;
weight: number;
product_warehouse_id?: number;
qty?: number;
weight?: number;
};
export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
+1 -1
View File
@@ -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;
+1
View File
@@ -5,6 +5,7 @@ import { Kandang } from '@/types/api/master-data/kandang';
export type HppPerKandangRow = {
id: number;
kandang: Kandang;
name_with_periode?: string;
weight_range: {
weight_min: number;
weight_max: number;