mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
Merge branch 'development' into fix/transfer-to-laying
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<
|
||||
|
||||
+30
-12
@@ -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);
|
||||
}
|
||||
|
||||
+5
-5
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,7 @@ export class ChickinService extends BaseApiService<
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error approve chickin:', error);
|
||||
return undefined;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
Vendored
+9
@@ -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;
|
||||
|
||||
Vendored
+2
-2
@@ -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;
|
||||
}[];
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user