mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +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) => {
|
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) {
|
if (minDate && selectedDate < minDate) {
|
||||||
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
|
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
|
||||||
return;
|
return;
|
||||||
@@ -136,7 +144,15 @@ const DateInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectRange = (range?: { from?: Date; to?: Date }) => {
|
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);
|
setSelectedRange(range);
|
||||||
|
|
||||||
const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : '';
|
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 ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
|
||||||
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
||||||
|
import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable';
|
||||||
|
import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable';
|
||||||
|
|
||||||
interface ClosingSapronakTableProps {
|
interface ClosingSapronakTableProps {
|
||||||
projectFlockId?: number;
|
projectFlockId?: number;
|
||||||
@@ -16,7 +18,15 @@ const ClosingSapronakTabContent = ({
|
|||||||
<>
|
<>
|
||||||
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
|
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
|
||||||
|
|
||||||
|
<ClosingIncomingSapronaksSummaryTable
|
||||||
|
projectFlockId={projectFlockId}
|
||||||
|
/>
|
||||||
|
|
||||||
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
|
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
|
||||||
|
|
||||||
|
<ClosingOutgoingSapronaksSummaryTable
|
||||||
|
projectFlockId={projectFlockId}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,12 +82,12 @@ const SalesReportTable = ({
|
|||||||
<div className='font-semibold text-gray-900'>Total Penjualan</div>
|
<div className='font-semibold text-gray-900'>Total Penjualan</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: 'age',
|
// id: 'age',
|
||||||
accessorKey: 'age',
|
// accessorKey: 'age',
|
||||||
header: 'Umur',
|
// header: 'Umur',
|
||||||
cell: (props) => props.getValue() || '-',
|
// cell: (props) => props.getValue() || '-',
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
id: 'do_number',
|
id: 'do_number',
|
||||||
accessorKey: 'do_number',
|
accessorKey: 'do_number',
|
||||||
|
|||||||
@@ -253,7 +253,6 @@ export const generateDashboardPDF = async ({
|
|||||||
|
|
||||||
toast.success('PDF exported successfully!', { id: 'export-pdf' });
|
toast.success('PDF exported successfully!', { id: 'export-pdf' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating PDF:', error);
|
|
||||||
toast.error('Failed to export PDF. Please try again.', {
|
toast.error('Failed to export PDF. Please try again.', {
|
||||||
id: 'export-pdf',
|
id: 'export-pdf',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-7xl pb-16'>
|
<section className='w-full max-w-full pb-16'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href='/expense'
|
||||||
@@ -65,7 +65,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
tabs={expenseDetailTabs}
|
tabs={expenseDetailTabs}
|
||||||
variant='lifted'
|
variant='lifted'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'max-w-5xl mx-auto mt-4',
|
wrapper: 'mx-auto mt-4',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const ExpenseRealizationContent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
<RequirePermission permissions='lti.expense.update.realization'>
|
<RequirePermission permissions='lti.expense.update.realization'>
|
||||||
<Button
|
<Button
|
||||||
@@ -84,7 +84,7 @@ const ExpenseRealizationContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='overflow-x-auto w-full max-w-5xl mx-auto'>
|
<div className='overflow-x-auto w-full mx-auto'>
|
||||||
<table className='table table-sm table-zebra'>
|
<table className='table table-sm table-zebra'>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -179,7 +179,7 @@ const ExpenseRealizationContent = ({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div className='w-full mt-8 mx-auto'>
|
||||||
<div className='flex flex-row gap-4'>
|
<div className='flex flex-row gap-4'>
|
||||||
<Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}>
|
<Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}>
|
||||||
<div className='w-full flex flex-col gap-2'>
|
<div className='w-full flex flex-col gap-2'>
|
||||||
@@ -216,13 +216,15 @@ const ExpenseRealizationContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div className='w-full mt-8 mx-auto grid grid-cols-2 gap-4'>
|
||||||
|
<div>
|
||||||
<h2 className='font-bold text-xl text-center'>
|
<h2 className='font-bold text-xl text-center'>
|
||||||
Rincian Pengajuan Biaya Operasional
|
Rincian Pengajuan Biaya Operasional
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
{initialValues?.kandangs.map(
|
||||||
|
(kandangExpense, kandangExpenseIdx) => {
|
||||||
let expenseGrandTotal = 0;
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
kandangExpense.pengajuans?.forEach(
|
kandangExpense.pengajuans?.forEach(
|
||||||
@@ -258,7 +260,9 @@ const ExpenseRealizationContent = ({
|
|||||||
<td>{pengajuanItem.nonstock.name}</td>
|
<td>{pengajuanItem.nonstock.name}</td>
|
||||||
<td>{pengajuanItem.qty}</td>
|
<td>{pengajuanItem.qty}</td>
|
||||||
<td>{formatCurrency(pengajuanItem.price)}</td>
|
<td>{formatCurrency(pengajuanItem.price)}</td>
|
||||||
<td className='w-xs'>{pengajuanItem.note ?? '-'}</td>
|
<td className='w-xs'>
|
||||||
|
{pengajuanItem.notes ?? '-'}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -268,23 +272,27 @@ const ExpenseRealizationContent = ({
|
|||||||
<th colSpan={2} className='text-right'>
|
<th colSpan={2} className='text-right'>
|
||||||
Total Biaya Keseluruhan:
|
Total Biaya Keseluruhan:
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th>
|
<th colSpan={2}>
|
||||||
|
{formatCurrency(expenseGrandTotal)}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div>
|
||||||
<h2 className='font-bold text-xl text-center'>
|
<h2 className='font-bold text-xl text-center'>
|
||||||
Rincian Realisasi Biaya Operasional
|
Rincian Realisasi Biaya Operasional
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
{initialValues?.kandangs.map(
|
||||||
|
(kandangExpense, kandangExpenseIdx) => {
|
||||||
let expenseGrandTotal = 0;
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
kandangExpense.realisasi?.forEach(
|
kandangExpense.realisasi?.forEach(
|
||||||
@@ -320,7 +328,9 @@ const ExpenseRealizationContent = ({
|
|||||||
<td>{realisasiItem.nonstock.name}</td>
|
<td>{realisasiItem.nonstock.name}</td>
|
||||||
<td>{realisasiItem.qty}</td>
|
<td>{realisasiItem.qty}</td>
|
||||||
<td>{formatCurrency(realisasiItem.price)}</td>
|
<td>{formatCurrency(realisasiItem.price)}</td>
|
||||||
<td className='w-xs'>{realisasiItem.note ?? '-'}</td>
|
<td className='w-xs'>
|
||||||
|
{realisasiItem.notes ?? '-'}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
@@ -330,13 +340,17 @@ const ExpenseRealizationContent = ({
|
|||||||
<th colSpan={2} className='text-right'>
|
<th colSpan={2} className='text-right'>
|
||||||
Total Biaya Keseluruhan:
|
Total Biaya Keseluruhan:
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th>
|
<th colSpan={2}>
|
||||||
|
{formatCurrency(expenseGrandTotal)}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ const ExpenseRequestContent = ({
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{initialValues && !isLoadingApprovalHistory && approvalHistory && (
|
{initialValues && !isLoadingApprovalHistory && approvalHistory && (
|
||||||
<div className='w-full max-w-5xl my-4 mx-auto'>
|
<div className='w-full my-4 mx-auto'>
|
||||||
<ApprovalSteps approvals={approvalHistory} />
|
<ApprovalSteps approvals={approvalHistory} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -281,7 +281,7 @@ const ExpenseRequestContent = ({
|
|||||||
<div className='w-full mt-4 flex flex-col gap-4'>
|
<div className='w-full mt-4 flex flex-col gap-4'>
|
||||||
{/* TODO: apply RBAC */}
|
{/* TODO: apply RBAC */}
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||||
{isCurrentApprovalOnHeadArea && (
|
{isCurrentApprovalOnHeadArea && (
|
||||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||||
<Button
|
<Button
|
||||||
@@ -414,7 +414,7 @@ const ExpenseRequestContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='overflow-x-auto w-full max-w-5xl mx-auto'>
|
<div className='overflow-x-auto w-full mx-auto'>
|
||||||
<table className='table table-sm table-zebra'>
|
<table className='table table-sm table-zebra'>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -608,7 +608,7 @@ const ExpenseRequestContent = ({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div className='w-full mt-8 mx-auto'>
|
||||||
<h2 className='font-bold text-xl text-center'>
|
<h2 className='font-bold text-xl text-center'>
|
||||||
Rincian Pengajuan Biaya Operasional
|
Rincian Pengajuan Biaya Operasional
|
||||||
</h2>
|
</h2>
|
||||||
@@ -654,7 +654,7 @@ const ExpenseRequestContent = ({
|
|||||||
<td>{pengajuanItem.qty}</td>
|
<td>{pengajuanItem.qty}</td>
|
||||||
<td>{formatCurrency(pengajuanItem.price)}</td>
|
<td>{formatCurrency(pengajuanItem.price)}</td>
|
||||||
<td className='w-xs'>
|
<td className='w-xs'>
|
||||||
{pengajuanItem.note ?? '-'}
|
{pengajuanItem.notes ?? '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,17 +54,19 @@ const RowOptionsMenu = ({
|
|||||||
rejectClickHandler: () => void;
|
rejectClickHandler: () => void;
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const showEditButton =
|
const showEditButton = props.row.original.latest_approval
|
||||||
props.row.original.latest_approval.step_number !== 6 &&
|
? props.row.original.latest_approval.step_number !== 6 &&
|
||||||
(props.row.original.latest_approval.step_number === 1 ||
|
(props.row.original.latest_approval.step_number === 1 ||
|
||||||
props.row.original.latest_approval.step_number === 2 ||
|
props.row.original.latest_approval.step_number === 2 ||
|
||||||
props.row.original.latest_approval.step_number === 3 ||
|
props.row.original.latest_approval.step_number === 3 ||
|
||||||
props.row.original.latest_approval.step_number === 4);
|
props.row.original.latest_approval.step_number === 4)
|
||||||
|
: false;
|
||||||
|
|
||||||
// TODO: apply RBAC
|
// TODO: apply RBAC
|
||||||
const showRealizationButton =
|
const showRealizationButton = props.row.original.latest_approval
|
||||||
props.row.original.latest_approval.action !== 'REJECTED' &&
|
? props.row.original.latest_approval.action !== 'REJECTED' &&
|
||||||
props.row.original.latest_approval.step_number === 4;
|
props.row.original.latest_approval.step_number === 4
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
@@ -278,6 +280,7 @@ const ExpensesTable = () => {
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const isCheckboxDisabled =
|
const isCheckboxDisabled =
|
||||||
!row.getCanSelect() ||
|
!row.getCanSelect() ||
|
||||||
|
!row.original.latest_approval ||
|
||||||
row.original.latest_approval.action === 'REJECTED';
|
row.original.latest_approval.action === 'REJECTED';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -413,6 +416,8 @@ const ExpensesTable = () => {
|
|||||||
const tableEnableRowSelectionHandler: (row: Row<Expense>) => boolean = (
|
const tableEnableRowSelectionHandler: (row: Row<Expense>) => boolean = (
|
||||||
row
|
row
|
||||||
) => {
|
) => {
|
||||||
|
if (!row.original.latest_approval) return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
row.original.latest_approval.action !== 'REJECTED' &&
|
row.original.latest_approval.action !== 'REJECTED' &&
|
||||||
row.original.latest_approval.step_number !== 6
|
row.original.latest_approval.step_number !== 6
|
||||||
@@ -692,14 +697,6 @@ const ExpensesTable = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Cari Biaya Operasional'
|
|
||||||
value={tableFilterState.search}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-12 justify-end gap-2'>
|
<div className='grid grid-cols-12 justify-end gap-2'>
|
||||||
@@ -753,17 +750,12 @@ const ExpensesTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<DebouncedTextInput
|
||||||
label='Baris'
|
name='search'
|
||||||
options={ROWS_OPTIONS}
|
placeholder='Cari Biaya Operasional'
|
||||||
value={{
|
value={tableFilterState.search}
|
||||||
label: String(tableFilterState.pageSize),
|
onChange={searchChangeHandler}
|
||||||
value: tableFilterState.pageSize,
|
className={{ wrapper: 'col-span-12 max-w-52 justify-self-end' }}
|
||||||
}}
|
|
||||||
onChange={pageSizeChangeHandler}
|
|
||||||
className={{
|
|
||||||
wrapper: 'col-span-12 max-w-28 justify-self-end',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { isResponseSuccess } from '@/lib/api-helper';
|
|||||||
interface ExpenseKandangsTableProps {
|
interface ExpenseKandangsTableProps {
|
||||||
locationId?: number;
|
locationId?: number;
|
||||||
type: 'add' | 'edit' | 'detail';
|
type: 'add' | 'edit' | 'detail';
|
||||||
|
formType?: 'request' | 'realization';
|
||||||
selectedKandangs: {
|
selectedKandangs: {
|
||||||
id?: number;
|
id?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -31,6 +32,7 @@ interface ExpenseKandangsTableProps {
|
|||||||
|
|
||||||
const ExpenseKandangsTable = ({
|
const ExpenseKandangsTable = ({
|
||||||
type,
|
type,
|
||||||
|
formType = 'request',
|
||||||
locationId,
|
locationId,
|
||||||
selectedKandangs,
|
selectedKandangs,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -172,7 +174,16 @@ const ExpenseKandangsTable = ({
|
|||||||
updateSortingFilter('picSort', picSortFilter);
|
updateSortingFilter('picSort', picSortFilter);
|
||||||
}, [sorting, updateSortingFilter]);
|
}, [sorting, updateSortingFilter]);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{shouldShowTable && (
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
wrapper: className?.wrapper,
|
wrapper: className?.wrapper,
|
||||||
@@ -184,7 +195,11 @@ const ExpenseKandangsTable = ({
|
|||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
title={
|
title={
|
||||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
<div className='card-title'>Pilih Kandang</div>
|
<div className='card-title'>
|
||||||
|
{formType === 'realization'
|
||||||
|
? 'Kandang yang Direalisasikan'
|
||||||
|
: 'Pilih Kandang'}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
icon='material-symbols:keyboard-arrow-down'
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
@@ -235,6 +250,8 @@ const ExpenseKandangsTable = ({
|
|||||||
/>
|
/>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export const getExpenseRealizationFormInitialValues = (
|
|||||||
? formatDate(initialValues?.realization_date, 'YYYY-MM-DD')
|
? formatDate(initialValues?.realization_date, 'YYYY-MM-DD')
|
||||||
: undefined,
|
: undefined,
|
||||||
kandangs: initialValues?.kandangs.map((kandang) => ({
|
kandangs: initialValues?.kandangs.map((kandang) => ({
|
||||||
id: kandang.kandang_id,
|
id: kandang.id,
|
||||||
name: kandang.name,
|
name: kandang.name,
|
||||||
})),
|
})),
|
||||||
supplier: initialValues?.supplier
|
supplier: initialValues?.supplier
|
||||||
@@ -159,7 +159,7 @@ export const getExpenseRealizationFormInitialValues = (
|
|||||||
},
|
},
|
||||||
quantity: realisasiItem.qty,
|
quantity: realisasiItem.qty,
|
||||||
price: realisasiItem.price,
|
price: realisasiItem.price,
|
||||||
notes: realisasiItem.note,
|
notes: realisasiItem.notes,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
: kandangExpense.pengajuans
|
: kandangExpense.pengajuans
|
||||||
@@ -170,7 +170,7 @@ export const getExpenseRealizationFormInitialValues = (
|
|||||||
},
|
},
|
||||||
quantity: expenseItem.qty,
|
quantity: expenseItem.qty,
|
||||||
price: expenseItem.price,
|
price: expenseItem.price,
|
||||||
notes: expenseItem.note,
|
notes: expenseItem.notes,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ const ExpenseRealizationForm = ({
|
|||||||
}, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]);
|
}, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='w-full max-w-5xl'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href='/expense'
|
||||||
@@ -297,6 +297,7 @@ const ExpenseRealizationForm = ({
|
|||||||
|
|
||||||
<ExpenseKandangsTable
|
<ExpenseKandangsTable
|
||||||
type='detail'
|
type='detail'
|
||||||
|
formType='realization'
|
||||||
locationId={formik.values.location?.value}
|
locationId={formik.values.location?.value}
|
||||||
selectedKandangs={formik.values.kandangs ?? []}
|
selectedKandangs={formik.values.kandangs ?? []}
|
||||||
onChange={kandangsChangeHandler}
|
onChange={kandangsChangeHandler}
|
||||||
|
|||||||
@@ -41,22 +41,25 @@ type ExpenseFormSchemaType = {
|
|||||||
export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
||||||
Yup.object({
|
Yup.object({
|
||||||
category: Yup.object({
|
category: Yup.object({
|
||||||
value: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
|
value: Yup.string()
|
||||||
label: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
|
.oneOf(['BOP', 'NON-BOP'])
|
||||||
|
.required('Kategori wajib diisi!'),
|
||||||
|
label: Yup.string()
|
||||||
|
.oneOf(['BOP', 'NON-BOP'])
|
||||||
|
.required('Kategori wajib diisi!'),
|
||||||
})
|
})
|
||||||
.nullable()
|
.nullable()
|
||||||
.optional(),
|
.required('Kategori wajib diisi!')
|
||||||
|
.typeError('Kategori wajib diisi!'),
|
||||||
|
|
||||||
location: Yup.object({
|
location: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
})
|
}).nullable(),
|
||||||
.nullable()
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
location_id: Yup.number()
|
location_id: Yup.number()
|
||||||
.required('Lokasi wajib diisi!')
|
|
||||||
.min(1, 'Lokasi wajib diisi!')
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
|
.required('Lokasi wajib diisi!')
|
||||||
.typeError('Lokasi wajib diisi!'),
|
.typeError('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
|
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
|
||||||
@@ -73,9 +76,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
|||||||
supplier: Yup.object({
|
supplier: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
})
|
}).nullable(),
|
||||||
.nullable()
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
supplier_id: Yup.number()
|
supplier_id: Yup.number()
|
||||||
.required('Vendor wajib diisi!')
|
.required('Vendor wajib diisi!')
|
||||||
@@ -104,9 +105,12 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
|||||||
.of(
|
.of(
|
||||||
Yup.object({
|
Yup.object({
|
||||||
nonstock: Yup.object({
|
nonstock: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required('Nonstock wajib diisi!'),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required('Nonstock wajib diisi!'),
|
||||||
}).nullable(),
|
})
|
||||||
|
.nullable()
|
||||||
|
.required('Nonstock wajib diisi!')
|
||||||
|
.typeError('Nonstock wajib diisi!'),
|
||||||
nonstock_id: Yup.number()
|
nonstock_id: Yup.number()
|
||||||
.required('Nonstock wajib diisi!')
|
.required('Nonstock wajib diisi!')
|
||||||
.min(1, 'Nonstock wajib diisi!')
|
.min(1, 'Nonstock wajib diisi!')
|
||||||
@@ -204,7 +208,7 @@ export const getExpenseFormInitialValues = (
|
|||||||
nonstock_id: expenseItem.nonstock.id,
|
nonstock_id: expenseItem.nonstock.id,
|
||||||
quantity: expenseItem.qty,
|
quantity: expenseItem.qty,
|
||||||
price: expenseItem.price,
|
price: expenseItem.price,
|
||||||
notes: expenseItem.note,
|
notes: expenseItem.notes,
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -190,30 +190,18 @@ const ExpenseRequestForm = ({
|
|||||||
formik.setFieldValue('category', val);
|
formik.setFieldValue('category', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const locationChangeHandler = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const location = val as OptionType | null;
|
||||||
|
const locationId = location ? Number(location.value) : 0;
|
||||||
|
|
||||||
formik.setFieldTouched('location', true);
|
formik.setFieldTouched('location', true);
|
||||||
formik.setFieldValue('location', val);
|
formik.setFieldValue('location', location);
|
||||||
|
formik.setFieldTouched('location_id', true);
|
||||||
const locationId = Array.isArray(val) ? val[0]?.value : val?.value;
|
|
||||||
formik.setFieldValue('location_id', locationId);
|
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: '',
|
|
||||||
},
|
},
|
||||||
],
|
[]
|
||||||
},
|
);
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const kandangsChangeHandler = (
|
const kandangsChangeHandler = (
|
||||||
kandangs: { id?: number; name?: string }[]
|
kandangs: { id?: number; name?: string }[]
|
||||||
@@ -268,6 +256,7 @@ const ExpenseRequestForm = ({
|
|||||||
|
|
||||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched('supplier', true);
|
formik.setFieldTouched('supplier', true);
|
||||||
|
formik.setFieldTouched('supplier_id', true);
|
||||||
formik.setFieldValue('supplier', val);
|
formik.setFieldValue('supplier', val);
|
||||||
|
|
||||||
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
|
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
|
||||||
@@ -360,7 +349,7 @@ const ExpenseRequestForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-5xl'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href='/expense'
|
||||||
@@ -407,6 +396,16 @@ const ExpenseRequestForm = ({
|
|||||||
placeholder='Pilih Kategori'
|
placeholder='Pilih Kategori'
|
||||||
value={formik.values.category}
|
value={formik.values.category}
|
||||||
onChange={categoryChangeHandler}
|
onChange={categoryChangeHandler}
|
||||||
|
isError={
|
||||||
|
formik.touched.category && Boolean(formik.errors.category)
|
||||||
|
}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.category && formik.errors.category
|
||||||
|
? typeof formik.errors.category === 'object'
|
||||||
|
? 'Kategori wajib diisi!'
|
||||||
|
: (formik.errors.category as string)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 'BOP',
|
value: 'BOP',
|
||||||
@@ -427,8 +426,13 @@ const ExpenseRequestForm = ({
|
|||||||
value={formik.values.location}
|
value={formik.values.location}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isLoading={isLoadingLocationOptions}
|
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
isError={
|
||||||
|
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.location_id as string}
|
||||||
|
isClearable
|
||||||
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -438,6 +442,12 @@ const ExpenseRequestForm = ({
|
|||||||
required
|
required
|
||||||
value={formik.values.transaction_date}
|
value={formik.values.transaction_date}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.transaction_date &&
|
||||||
|
Boolean(formik.errors.transaction_date)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.transaction_date as string}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
}}
|
}}
|
||||||
@@ -460,8 +470,12 @@ const ExpenseRequestForm = ({
|
|||||||
value={formik.values.supplier}
|
value={formik.values.supplier}
|
||||||
onChange={supplierChangeHandler}
|
onChange={supplierChangeHandler}
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
isLoading={isLoadingVendorOptions}
|
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
|
isLoading={isLoadingVendorOptions}
|
||||||
|
isError={
|
||||||
|
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.supplier_id as string}
|
||||||
className={{ wrapper: 'col-span-12' }}
|
className={{ wrapper: 'col-span-12' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`,
|
||||||
|
true
|
||||||
|
);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||||
val
|
val
|
||||||
@@ -96,7 +100,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isExpenseRepeaterInputError = (
|
const isExpenseRepeaterInputError = (
|
||||||
column: 'nonstock' | 'quantity' | 'price' | 'notes',
|
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
||||||
kandangExpenseIdx: number,
|
kandangExpenseIdx: number,
|
||||||
expenseIdx: number
|
expenseIdx: number
|
||||||
) => {
|
) => {
|
||||||
@@ -105,11 +109,14 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
expenseIdx
|
expenseIdx
|
||||||
]?.[column] &&
|
]?.[column] &&
|
||||||
Boolean(
|
Boolean(
|
||||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx] &&
|
||||||
Object &&
|
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx] ===
|
||||||
|
'object' &&
|
||||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||||
expenseIdx
|
expenseIdx
|
||||||
] instanceof Object &&
|
] &&
|
||||||
|
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx]
|
||||||
|
.cost_items?.[expenseIdx] === 'object' &&
|
||||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||||
expenseIdx
|
expenseIdx
|
||||||
]?.[column]
|
]?.[column]
|
||||||
@@ -117,6 +124,32 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getExpenseRepeaterErrorMessage = (
|
||||||
|
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
||||||
|
kandangExpenseIdx: number,
|
||||||
|
expenseIdx: number
|
||||||
|
): string => {
|
||||||
|
const kandangError = formik.errors.expense_nonstocks?.[kandangExpenseIdx];
|
||||||
|
|
||||||
|
if (!kandangError || typeof kandangError !== 'object') return '';
|
||||||
|
|
||||||
|
if (!('cost_items' in kandangError)) return '';
|
||||||
|
|
||||||
|
const costItemsError = kandangError.cost_items?.[expenseIdx];
|
||||||
|
|
||||||
|
if (!costItemsError || typeof costItemsError !== 'object') return '';
|
||||||
|
|
||||||
|
const fieldError = costItemsError[column as keyof typeof costItemsError];
|
||||||
|
|
||||||
|
if (!fieldError) return '';
|
||||||
|
|
||||||
|
if (typeof fieldError === 'object' && fieldError !== null) {
|
||||||
|
return 'Nonstock wajib diisi!';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(fieldError);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
@@ -202,10 +235,21 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
val
|
val
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
isError={isExpenseRepeaterInputError(
|
||||||
|
'nonstock_id',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'nonstock_id',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
options={nonstockOptions}
|
options={nonstockOptions}
|
||||||
isLoading={isLoadingNonstockOptions}
|
isLoading={isLoadingNonstockOptions}
|
||||||
onInputChange={setNonstockInputValue}
|
onInputChange={setNonstockInputValue}
|
||||||
className={{ wrapper: 'min-w-48' }}
|
className={{ wrapper: 'min-w-48' }}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -226,6 +270,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
kandangExpenseIdx,
|
kandangExpenseIdx,
|
||||||
expenseIdx
|
expenseIdx
|
||||||
)}
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'quantity',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
className={{ wrapper: 'min-w-24' }}
|
className={{ wrapper: 'min-w-24' }}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@@ -246,6 +295,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
kandangExpenseIdx,
|
kandangExpenseIdx,
|
||||||
expenseIdx
|
expenseIdx
|
||||||
)}
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'price',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
inputPrefix={
|
inputPrefix={
|
||||||
<span className='text-gray-600 font-medium'>
|
<span className='text-gray-600 font-medium'>
|
||||||
Rp
|
Rp
|
||||||
@@ -271,6 +325,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
kandangExpenseIdx,
|
kandangExpenseIdx,
|
||||||
expenseIdx
|
expenseIdx
|
||||||
)}
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'notes',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
className={{ wrapper: 'min-w-24' }}
|
className={{ wrapper: 'min-w-24' }}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
{pengajuan.note}
|
{pengajuan.notes}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -607,7 +607,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
{realisasi.note}
|
{realisasi.notes}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Nominal',
|
label: 'Nominal',
|
||||||
value: formatCurrency(finance.nominal),
|
value: formatCurrency(Math.abs(finance.nominal)),
|
||||||
},
|
},
|
||||||
].filter((item) => {
|
].filter((item) => {
|
||||||
// Hide party account number row if transaction type is INJECTION
|
// Hide party account number row if transaction type is INJECTION
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
FINANCE_INITIAL_BALANCE_STATUS,
|
FINANCE_INITIAL_BALANCE_STATUS,
|
||||||
FINANCE_INJECTION_STATUS,
|
FINANCE_INJECTION_STATUS,
|
||||||
FINANCE_TRANSACTION_STATUS,
|
FINANCE_TRANSACTION_STATUS,
|
||||||
|
FINANCE_TRANSACTION_TYPE_OPTIONS,
|
||||||
} from '@/config/constant';
|
} from '@/config/constant';
|
||||||
import { FinanceApi } from '@/services/api/finance';
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -65,8 +66,7 @@ const RowOptionsMenu = ({
|
|||||||
|
|
||||||
{FINANCE_TRANSACTION_STATUS.includes(
|
{FINANCE_TRANSACTION_STATUS.includes(
|
||||||
props.row.original.transaction_type
|
props.row.original.transaction_type
|
||||||
) &&
|
) && (
|
||||||
props.row.original.party?.type !== 'SUPPLIER' && (
|
|
||||||
<RequirePermission permissions='lti.finance.payments.update'>
|
<RequirePermission permissions='lti.finance.payments.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
|
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
|
||||||
@@ -74,11 +74,7 @@ const RowOptionsMenu = ({
|
|||||||
color='warning'
|
color='warning'
|
||||||
className='justify-start text-sm'
|
className='justify-start text-sm'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
icon='material-symbols:edit-outline'
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
@@ -148,7 +144,8 @@ const FinanceTable = () => {
|
|||||||
search: '',
|
search: '',
|
||||||
transactionType: '',
|
transactionType: '',
|
||||||
bankId: '',
|
bankId: '',
|
||||||
partyType: '',
|
customerId: '',
|
||||||
|
supplierId: '',
|
||||||
sortBy: '',
|
sortBy: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
@@ -158,7 +155,8 @@ const FinanceTable = () => {
|
|||||||
pageSize: 'limit',
|
pageSize: 'limit',
|
||||||
transactionType: 'transaction_type',
|
transactionType: 'transaction_type',
|
||||||
bankId: 'bank_id',
|
bankId: 'bank_id',
|
||||||
partyType: 'party_type',
|
customerId: 'customer_id',
|
||||||
|
supplierId: 'supplier_id',
|
||||||
sortBy: 'sort_date',
|
sortBy: 'sort_date',
|
||||||
startDate: 'start_date',
|
startDate: 'start_date',
|
||||||
endDate: 'end_date',
|
endDate: 'end_date',
|
||||||
@@ -172,17 +170,24 @@ const FinanceTable = () => {
|
|||||||
search: '',
|
search: '',
|
||||||
transactionType: '',
|
transactionType: '',
|
||||||
bankId: '',
|
bankId: '',
|
||||||
partyType: '',
|
customerId: '',
|
||||||
|
supplierId: '',
|
||||||
sortBy: '',
|
sortBy: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
});
|
});
|
||||||
const [selectedTransactionType, setSelectedTransactionType] =
|
const [selectedTransactionType, setSelectedTransactionType] = useState<
|
||||||
useState<OptionType | null>(null);
|
OptionType | OptionType[] | null
|
||||||
const [selectedBank, setSelectedBank] = useState<OptionType | null>(null);
|
>(null);
|
||||||
const [selectedPartyType, setSelectedPartyType] = useState<OptionType | null>(
|
const [selectedBank, setSelectedBank] = useState<
|
||||||
null
|
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 [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
|
||||||
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
|
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
@@ -197,27 +202,18 @@ const FinanceTable = () => {
|
|||||||
FinanceApi.getAllFetcher
|
FinanceApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== Options =====
|
|
||||||
const transactionTypeOptions = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{ label: 'Customer', value: 'CUSTOMER' },
|
|
||||||
{ label: 'Supplier', value: 'SUPPLIER' },
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
const {
|
const {
|
||||||
options: partyTypeOptions,
|
options: customerOptions,
|
||||||
isLoadingOptions: partyTypeIsLoadingOptions,
|
isLoadingOptions: customerIsLoadingOptions,
|
||||||
setInputValue: partyTypeInputValue,
|
setInputValue: customerInputValue,
|
||||||
loadMore: partyTypeLoadMore,
|
loadMore: customerLoadMore,
|
||||||
} = useSelect(
|
} = useSelect(CustomerApi.basePath, 'id', 'name');
|
||||||
selectedTransactionType
|
const {
|
||||||
? selectedTransactionType.value === 'CUSTOMER'
|
options: supplierOptions,
|
||||||
? CustomerApi.basePath
|
isLoadingOptions: supplierIsLoadingOptions,
|
||||||
: SupplierApi.basePath
|
setInputValue: supplierInputValue,
|
||||||
: '',
|
loadMore: supplierLoadMore,
|
||||||
'id',
|
} = useSelect(SupplierApi.basePath, 'id', 'name');
|
||||||
'name'
|
|
||||||
);
|
|
||||||
const sortByOptions = useMemo(() => {
|
const sortByOptions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
|
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
|
||||||
@@ -238,24 +234,47 @@ const FinanceTable = () => {
|
|||||||
const transactionTypeChangeHandler = (
|
const transactionTypeChangeHandler = (
|
||||||
val: OptionType | OptionType[] | null
|
val: OptionType | OptionType[] | null
|
||||||
) => {
|
) => {
|
||||||
setSelectedTransactionType(val as OptionType);
|
setSelectedTransactionType(val);
|
||||||
setPendingFilters((prev) => ({
|
setPendingFilters((prev) => ({
|
||||||
...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) => {
|
const bankChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
setSelectedBank(val as OptionType);
|
setSelectedBank(val);
|
||||||
setPendingFilters((prev) => ({
|
setPendingFilters((prev) => ({
|
||||||
...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) => {
|
const customerIdChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
setSelectedPartyType(val as OptionType);
|
setSelectedCustomerId(val);
|
||||||
setPendingFilters((prev) => ({
|
setPendingFilters((prev) => ({
|
||||||
...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) => {
|
const sortByChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -279,7 +298,8 @@ const FinanceTable = () => {
|
|||||||
updateFilter('search', pendingFilters.search);
|
updateFilter('search', pendingFilters.search);
|
||||||
updateFilter('transactionType', pendingFilters.transactionType);
|
updateFilter('transactionType', pendingFilters.transactionType);
|
||||||
updateFilter('bankId', pendingFilters.bankId);
|
updateFilter('bankId', pendingFilters.bankId);
|
||||||
updateFilter('partyType', pendingFilters.partyType);
|
updateFilter('customerId', pendingFilters.customerId);
|
||||||
|
updateFilter('supplierId', pendingFilters.supplierId);
|
||||||
updateFilter('sortBy', pendingFilters.sortBy);
|
updateFilter('sortBy', pendingFilters.sortBy);
|
||||||
updateFilter('startDate', pendingFilters.startDate);
|
updateFilter('startDate', pendingFilters.startDate);
|
||||||
updateFilter('endDate', pendingFilters.endDate);
|
updateFilter('endDate', pendingFilters.endDate);
|
||||||
@@ -287,14 +307,16 @@ const FinanceTable = () => {
|
|||||||
const resetFilterHandler = () => {
|
const resetFilterHandler = () => {
|
||||||
setSelectedTransactionType(null);
|
setSelectedTransactionType(null);
|
||||||
setSelectedBank(null);
|
setSelectedBank(null);
|
||||||
setSelectedPartyType(null);
|
setSelectedCustomerId(null);
|
||||||
|
setSelectedSupplierId(null);
|
||||||
setSelectedSortBy(null);
|
setSelectedSortBy(null);
|
||||||
|
|
||||||
const emptyFilters = {
|
const emptyFilters = {
|
||||||
search: '',
|
search: '',
|
||||||
transactionType: '',
|
transactionType: '',
|
||||||
bankId: '',
|
bankId: '',
|
||||||
partyType: '',
|
customerId: '',
|
||||||
|
supplierId: '',
|
||||||
sortBy: '',
|
sortBy: '',
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: '',
|
endDate: '',
|
||||||
@@ -304,7 +326,8 @@ const FinanceTable = () => {
|
|||||||
updateFilter('search', '');
|
updateFilter('search', '');
|
||||||
updateFilter('transactionType', '');
|
updateFilter('transactionType', '');
|
||||||
updateFilter('bankId', '');
|
updateFilter('bankId', '');
|
||||||
updateFilter('partyType', '');
|
updateFilter('customerId', '');
|
||||||
|
updateFilter('supplierId', '');
|
||||||
updateFilter('sortBy', '');
|
updateFilter('sortBy', '');
|
||||||
updateFilter('startDate', '');
|
updateFilter('startDate', '');
|
||||||
updateFilter('endDate', '');
|
updateFilter('endDate', '');
|
||||||
@@ -477,27 +500,34 @@ const FinanceTable = () => {
|
|||||||
>
|
>
|
||||||
<div className='grid grid-cols-4 gap-6'>
|
<div className='grid grid-cols-4 gap-6'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={transactionTypeOptions}
|
options={FINANCE_TRANSACTION_TYPE_OPTIONS}
|
||||||
label='Tipe Transaksi'
|
label='Jenis Transaksi'
|
||||||
value={selectedTransactionType}
|
value={selectedTransactionType}
|
||||||
onChange={transactionTypeChangeHandler}
|
onChange={transactionTypeChangeHandler}
|
||||||
isClearable
|
isClearable
|
||||||
|
isMulti
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={partyTypeOptions}
|
options={customerOptions}
|
||||||
label={
|
label={'Customer'}
|
||||||
selectedTransactionType
|
value={selectedCustomerId}
|
||||||
? selectedTransactionType.value === 'CUSTOMER'
|
onChange={customerIdChangeHandler}
|
||||||
? 'Pelanggan'
|
onInputChange={customerInputValue}
|
||||||
: 'Supplier'
|
onMenuScrollToBottom={customerLoadMore}
|
||||||
: 'Pihak'
|
isLoading={customerIsLoadingOptions}
|
||||||
}
|
|
||||||
value={selectedPartyType}
|
|
||||||
onChange={partyTypeChangeHandler}
|
|
||||||
onInputChange={partyTypeInputValue}
|
|
||||||
onMenuScrollToBottom={partyTypeLoadMore}
|
|
||||||
isLoading={partyTypeIsLoadingOptions}
|
|
||||||
isClearable
|
isClearable
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
options={supplierOptions}
|
||||||
|
label={'Supplier'}
|
||||||
|
value={selectedSupplierId}
|
||||||
|
onChange={supplierIdChangeHandler}
|
||||||
|
onInputChange={supplierInputValue}
|
||||||
|
onMenuScrollToBottom={supplierLoadMore}
|
||||||
|
isLoading={supplierIsLoadingOptions}
|
||||||
|
isClearable
|
||||||
|
isMulti
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={
|
options={
|
||||||
@@ -522,13 +552,7 @@ const FinanceTable = () => {
|
|||||||
onInputChange={bankInputValue}
|
onInputChange={bankInputValue}
|
||||||
onMenuScrollToBottom={bankLoadMore}
|
onMenuScrollToBottom={bankLoadMore}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
isMulti
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
label='Cari'
|
|
||||||
placeholder='Cari'
|
|
||||||
value={pendingFilters.search}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={sortByOptions}
|
options={sortByOptions}
|
||||||
@@ -549,6 +573,13 @@ const FinanceTable = () => {
|
|||||||
value={pendingFilters.endDate}
|
value={pendingFilters.endDate}
|
||||||
onChange={endDateChangeHandler}
|
onChange={endDateChangeHandler}
|
||||||
/>
|
/>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
label='Cari'
|
||||||
|
placeholder='Cari'
|
||||||
|
value={pendingFilters.search}
|
||||||
|
onChange={searchChangeHandler}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Table<Finance>
|
<Table<Finance>
|
||||||
|
|||||||
@@ -245,7 +245,11 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
isClearable
|
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
|
<SelectInput
|
||||||
label='Bank'
|
label='Bank'
|
||||||
@@ -323,6 +327,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
|
isDisabled={type == 'edit'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label='Nominal'
|
label='Nominal'
|
||||||
|
|||||||
@@ -110,6 +110,14 @@ const DeliveryProductObjectSchema = Yup.object({
|
|||||||
.typeError('Qty harus berupa angka!'),
|
.typeError('Qty harus berupa angka!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DeliveryDocumentSchema = Yup.mixed<File | MovementDocument>()
|
||||||
|
.nullable()
|
||||||
|
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value): boolean => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
||||||
delivery_cost: Yup.number()
|
delivery_cost: Yup.number()
|
||||||
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
||||||
@@ -135,13 +143,7 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
|||||||
}),
|
}),
|
||||||
document_path: Yup.string().nullable().optional(),
|
document_path: Yup.string().nullable().optional(),
|
||||||
document_index: Yup.number().optional(),
|
document_index: Yup.number().optional(),
|
||||||
document: Yup.mixed<File | MovementDocument>()
|
document: DeliveryDocumentSchema,
|
||||||
.nullable()
|
|
||||||
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
|
|
||||||
if (!value) return true;
|
|
||||||
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
driver_name: Yup.string().required('Nama sopir wajib diisi!'),
|
driver_name: Yup.string().required('Nama sopir wajib diisi!'),
|
||||||
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
|
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
|
||||||
supplier: Yup.object({
|
supplier: Yup.object({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
@@ -95,7 +95,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
isLoadingOptions: isLoadingWarehouses,
|
isLoadingOptions: isLoadingWarehouses,
|
||||||
loadMore: loadMoreWarehouses,
|
loadMore: loadMoreWarehouses,
|
||||||
rawData: warehouses,
|
rawData: warehouses,
|
||||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search');
|
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search', {
|
||||||
|
flag: 'EKSPEDISI',
|
||||||
|
});
|
||||||
|
|
||||||
// ===== SELECT INPUT DATA =====
|
// ===== SELECT INPUT DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -261,6 +263,47 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const prevSourceWarehouseIdRef = useRef<number | null>(
|
||||||
|
formik.values.source_warehouse_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== RESET PRODUCTS WHEN SOURCE WAREHOUSE CHANGES =====
|
||||||
|
useEffect(() => {
|
||||||
|
const prevSourceWarehouseId = prevSourceWarehouseIdRef.current;
|
||||||
|
const currentSourceWarehouseId = formik.values.source_warehouse_id;
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
||||||
|
prevSourceWarehouseId !== null
|
||||||
|
) {
|
||||||
|
formik.setFieldValue('products', [
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
formik.setFieldTouched('products', false);
|
||||||
|
|
||||||
|
const updatedDeliveries = formik.values.deliveries.map(
|
||||||
|
(delivery: DeliverySchema) => ({
|
||||||
|
...delivery,
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
|
formik.setFieldTouched('deliveries', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
||||||
|
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
|
||||||
|
|
||||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setProductWarehouseSelectInputValue,
|
setInputValue: setProductWarehouseSelectInputValue,
|
||||||
@@ -347,13 +390,71 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTransferDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
formik.setFieldValue('transfer_date', e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== EVENT HANDLERS =====
|
// ===== EVENT HANDLERS =====
|
||||||
// Product Handlers
|
const handleTransferDateChange = useCallback(
|
||||||
const addProduct = () => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
formik.setFieldValue('transfer_date', e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSourceWarehouseChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newSourceWarehouseId = (val as WarehouseOptionType)?.value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newSourceWarehouseId &&
|
||||||
|
newSourceWarehouseId === formik.values.destination_warehouse_id
|
||||||
|
) {
|
||||||
|
const destinationWarehouseName =
|
||||||
|
(formik.values.destination_warehouse as WarehouseOptionType)?.label ||
|
||||||
|
'gudang tujuan';
|
||||||
|
toast.error(
|
||||||
|
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.setFieldTouched('source_warehouse', true);
|
||||||
|
formik.setFieldValue('source_warehouse', val);
|
||||||
|
formik.setFieldTouched('source_warehouse_id', true);
|
||||||
|
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
formik.values.destination_warehouse_id,
|
||||||
|
formik.values.destination_warehouse,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDestinationWarehouseChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newDestinationWarehouseId = (val as WarehouseOptionType)?.value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newDestinationWarehouseId &&
|
||||||
|
newDestinationWarehouseId === formik.values.source_warehouse_id
|
||||||
|
) {
|
||||||
|
const sourceWarehouseName =
|
||||||
|
(formik.values.source_warehouse as WarehouseOptionType)?.label ||
|
||||||
|
'gudang asal';
|
||||||
|
toast.error(
|
||||||
|
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.setFieldTouched('destination_warehouse', true);
|
||||||
|
formik.setFieldValue('destination_warehouse', val);
|
||||||
|
formik.setFieldTouched('destination_warehouse_id', true);
|
||||||
|
formik.setFieldValue(
|
||||||
|
'destination_warehouse_id',
|
||||||
|
newDestinationWarehouseId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[formik.values.source_warehouse_id, formik.values.source_warehouse]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addProduct = useCallback(() => {
|
||||||
const newProducts = [
|
const newProducts = [
|
||||||
...(formik.values.products || []),
|
...(formik.values.products || []),
|
||||||
{
|
{
|
||||||
@@ -363,10 +464,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
formik.setFieldValue('products', newProducts);
|
formik.setFieldValue('products', newProducts);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const removeProduct = useCallback(
|
const removeProduct = useCallback((i: number) => {
|
||||||
(i: number) => {
|
|
||||||
const updatedProducts =
|
const updatedProducts =
|
||||||
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
||||||
if (index !== i) {
|
if (index !== i) {
|
||||||
@@ -376,9 +476,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}, []) ?? [];
|
}, []) ?? [];
|
||||||
|
|
||||||
formik.setFieldValue('products', updatedProducts);
|
formik.setFieldValue('products', updatedProducts);
|
||||||
},
|
}, []);
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const bulkRemoveProduct = useCallback(() => {
|
const bulkRemoveProduct = useCallback(() => {
|
||||||
const updatedProducts =
|
const updatedProducts =
|
||||||
@@ -387,10 +485,45 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
) ?? [];
|
) ?? [];
|
||||||
formik.setFieldValue('products', updatedProducts);
|
formik.setFieldValue('products', updatedProducts);
|
||||||
setSelectedProducts([]);
|
setSelectedProducts([]);
|
||||||
}, [formik, selectedProducts]);
|
}, [formik, selectedProducts, setSelectedProducts]);
|
||||||
|
|
||||||
// Delivery Handlers
|
const handleProductChange = useCallback(
|
||||||
const addDelivery = () => {
|
(idx: number, val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched(`products.${idx}.product`, true);
|
||||||
|
formik.setFieldValue(`products.${idx}.product`, val);
|
||||||
|
formik.setFieldTouched(`products.${idx}.product_id`, true);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`products.${idx}.product_id`,
|
||||||
|
(val as ProductWarehouseOptionType)?.value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProductSelectAllChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedProducts(formik.values.products?.map((_, idx) => idx) ?? []);
|
||||||
|
} else {
|
||||||
|
setSelectedProducts([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[formik.values.products, setSelectedProducts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProductCheckboxChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const idx = Number(e.target.name.replace('product-', ''));
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedProducts((prev) => [...prev, idx]);
|
||||||
|
} else {
|
||||||
|
setSelectedProducts((prev) => prev.filter((i) => i !== idx));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSelectedProducts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addDelivery = useCallback(() => {
|
||||||
formik.setFieldValue('deliveries', [
|
formik.setFieldValue('deliveries', [
|
||||||
...(formik.values.deliveries || []),
|
...(formik.values.deliveries || []),
|
||||||
{
|
{
|
||||||
@@ -410,25 +543,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const removeDelivery = useCallback(
|
const removeDelivery = useCallback((i: number) => {
|
||||||
(i: number) => {
|
|
||||||
const updatedDeliveries =
|
const updatedDeliveries =
|
||||||
formik.values.deliveries?.reduce(
|
formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => {
|
||||||
(acc: DeliverySchema[], item, index) => {
|
|
||||||
if (index !== i) {
|
if (index !== i) {
|
||||||
acc.push(item);
|
acc.push(item);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
},
|
}, []) ?? [];
|
||||||
[]
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
},
|
}, []);
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const bulkRemoveDelivery = useCallback(() => {
|
const bulkRemoveDelivery = useCallback(() => {
|
||||||
const updatedDeliveries =
|
const updatedDeliveries =
|
||||||
@@ -437,11 +564,81 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
) ?? [];
|
) ?? [];
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
setSelectedDeliveries([]);
|
setSelectedDeliveries([]);
|
||||||
}, [formik, selectedDeliveries]);
|
}, [formik, selectedDeliveries, setSelectedDeliveries]);
|
||||||
|
|
||||||
// Cost Calculation Handlers
|
const handleDeliverySelectAllChange = useCallback(
|
||||||
const handleDeliveryCostChange = useCallback(
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(idx: number, value: number) => {
|
if (e.target.checked) {
|
||||||
|
setSelectedDeliveries(
|
||||||
|
formik.values.deliveries?.map((_, idx) => idx) ?? []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSelectedDeliveries([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[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);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
||||||
|
|
||||||
const delivery = formik.values.deliveries?.[idx];
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
@@ -460,9 +657,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDeliveryCostPerItemChange = useCallback(
|
const handleDeliveryCostPerItemChange = useCallback(
|
||||||
(idx: number, value: number) => {
|
(idx: number, value: number) => {
|
||||||
@@ -482,7 +677,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostChangeWrapper = useCallback(
|
const handleDeliveryCostChangeWrapper = useCallback(
|
||||||
@@ -957,43 +1152,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih gudang asal...'
|
placeholder='Pilih gudang asal...'
|
||||||
value={formik.values.source_warehouse}
|
value={formik.values.source_warehouse}
|
||||||
onChange={(val) => {
|
onChange={handleSourceWarehouseChange}
|
||||||
const newSourceWarehouseId = (val as WarehouseOptionType)
|
|
||||||
?.value;
|
|
||||||
|
|
||||||
if (newSourceWarehouseId) {
|
|
||||||
if (
|
|
||||||
newSourceWarehouseId ===
|
|
||||||
formik.values.destination_warehouse_id
|
|
||||||
) {
|
|
||||||
const destinationWarehouseName =
|
|
||||||
(
|
|
||||||
formik.values
|
|
||||||
.destination_warehouse as WarehouseOptionType
|
|
||||||
)?.label || 'gudang tujuan';
|
|
||||||
|
|
||||||
toast.error(
|
|
||||||
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.setFieldTouched('source_warehouse', true);
|
|
||||||
formik.setFieldValue('source_warehouse', val);
|
|
||||||
formik.setFieldTouched('source_warehouse_id', true);
|
|
||||||
formik.setFieldValue(
|
|
||||||
'source_warehouse_id',
|
|
||||||
newSourceWarehouseId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
formik.errors.destination_warehouse_id ===
|
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
|
||||||
) {
|
|
||||||
formik.setFieldError('destination_warehouse_id', undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
onInputChange={setWarehouseSelectInputValue}
|
onInputChange={setWarehouseSelectInputValue}
|
||||||
onMenuScrollToBottom={loadMoreWarehouses}
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
@@ -1057,41 +1216,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih gudang tujuan...'
|
placeholder='Pilih gudang tujuan...'
|
||||||
value={formik.values.destination_warehouse}
|
value={formik.values.destination_warehouse}
|
||||||
onChange={(val) => {
|
onChange={handleDestinationWarehouseChange}
|
||||||
const newDestinationWarehouseId = (val as WarehouseOptionType)
|
|
||||||
?.value;
|
|
||||||
|
|
||||||
if (newDestinationWarehouseId) {
|
|
||||||
if (
|
|
||||||
newDestinationWarehouseId ===
|
|
||||||
formik.values.source_warehouse_id
|
|
||||||
) {
|
|
||||||
const sourceWarehouseName =
|
|
||||||
(formik.values.source_warehouse as WarehouseOptionType)
|
|
||||||
?.label || 'gudang asal';
|
|
||||||
|
|
||||||
toast.error(
|
|
||||||
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.setFieldTouched('destination_warehouse', true);
|
|
||||||
formik.setFieldValue('destination_warehouse', val);
|
|
||||||
formik.setFieldTouched('destination_warehouse_id', true);
|
|
||||||
formik.setFieldValue(
|
|
||||||
'destination_warehouse_id',
|
|
||||||
newDestinationWarehouseId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
formik.errors.destination_warehouse_id ===
|
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
|
||||||
) {
|
|
||||||
formik.setFieldError('destination_warehouse_id', undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
onInputChange={setWarehouseSelectInputValue}
|
onInputChange={setWarehouseSelectInputValue}
|
||||||
isLoading={isLoadingWarehouses}
|
isLoading={isLoadingWarehouses}
|
||||||
@@ -1165,18 +1290,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
selectedProducts.length &&
|
selectedProducts.length &&
|
||||||
formik.values.products?.length > 0
|
formik.values.products?.length > 0
|
||||||
}
|
}
|
||||||
onChange={(
|
onChange={handleProductSelectAllChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedProducts(
|
|
||||||
formik.values.products?.map((_, idx) => idx) ??
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedProducts([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1213,17 +1327,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name={`product-${idx}`}
|
name={`product-${idx}`}
|
||||||
checked={selectedProducts.includes(idx)}
|
checked={selectedProducts.includes(idx)}
|
||||||
onChange={(
|
onChange={handleProductCheckboxChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedProducts([...selectedProducts, idx]);
|
|
||||||
} else {
|
|
||||||
setSelectedProducts(
|
|
||||||
selectedProducts.filter((i) => i !== idx)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1235,24 +1339,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
value={product.product ?? undefined}
|
value={product.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) => handleProductChange(idx, val)}
|
||||||
formik.setFieldTouched(
|
|
||||||
`products.${idx}.product`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`products.${idx}.product`,
|
|
||||||
val
|
|
||||||
);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`products.${idx}.product_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`products.${idx}.product_id`,
|
|
||||||
(val as ProductWarehouseOptionType)?.value
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={productWarehouseOptions}
|
options={productWarehouseOptions}
|
||||||
onInputChange={setProductWarehouseSelectInputValue}
|
onInputChange={setProductWarehouseSelectInputValue}
|
||||||
onMenuScrollToBottom={loadMoreProductWarehouses}
|
onMenuScrollToBottom={loadMoreProductWarehouses}
|
||||||
@@ -1379,19 +1466,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
selectedDeliveries.length &&
|
selectedDeliveries.length &&
|
||||||
formik.values.deliveries?.length > 0
|
formik.values.deliveries?.length > 0
|
||||||
}
|
}
|
||||||
onChange={(
|
onChange={handleDeliverySelectAllChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedDeliveries(
|
|
||||||
formik.values.deliveries?.map(
|
|
||||||
(_, idx) => idx
|
|
||||||
) ?? []
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedDeliveries([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1474,20 +1549,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name={`delivery-${idx}`}
|
name={`delivery-${idx}`}
|
||||||
checked={selectedDeliveries.includes(idx)}
|
checked={selectedDeliveries.includes(idx)}
|
||||||
onChange={(
|
onChange={handleDeliveryCheckboxChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedDeliveries([
|
|
||||||
...selectedDeliveries,
|
|
||||||
idx,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setSelectedDeliveries(
|
|
||||||
selectedDeliveries.filter((i) => i !== idx)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1500,24 +1562,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
placeholder='Pilih produk...'
|
placeholder='Pilih produk...'
|
||||||
value={delivery.products[0]?.product ?? undefined}
|
value={delivery.products[0]?.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
formik.setFieldTouched(
|
handleDeliveryProductChange(idx, val)
|
||||||
`deliveries.${idx}.products.0.product`,
|
}
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.products.0.product`,
|
|
||||||
val
|
|
||||||
);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`deliveries.${idx}.products.0.product_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.products.0.product_id`,
|
|
||||||
(val as OptionType)?.value
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={getFilteredProductWarehouseOptions()}
|
options={getFilteredProductWarehouseOptions()}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
@@ -1568,24 +1615,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
placeholder='Pilih supplier...'
|
placeholder='Pilih supplier...'
|
||||||
value={delivery.supplier}
|
value={delivery.supplier}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
formik.setFieldTouched(
|
handleDeliverySupplierChange(idx, val)
|
||||||
`deliveries.${idx}.supplier`,
|
}
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.supplier`,
|
|
||||||
val
|
|
||||||
);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`deliveries.${idx}.supplier_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.supplier_id`,
|
|
||||||
(val as OptionType)?.value
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
onInputChange={setSupplierSelectInputValue}
|
onInputChange={setSupplierSelectInputValue}
|
||||||
isLoading={isLoadingSuppliers}
|
isLoading={isLoadingSuppliers}
|
||||||
@@ -1677,20 +1709,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<FileInput
|
<FileInput
|
||||||
accept='.pdf,.jpg,.jpeg,.png'
|
accept='.pdf,.jpg,.jpeg,.png'
|
||||||
name={`deliveries.${idx}.document`}
|
name={`deliveries.${idx}.document`}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
const file = e.target.files?.[0];
|
handleDeliveryDocumentChange(idx, e)
|
||||||
if (file) {
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
toast.error('Ukuran dokumen maksimal 5 MB!');
|
|
||||||
e.target.value = '';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.document`,
|
|
||||||
file
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...isRepeaterInputError(
|
{...isRepeaterInputError(
|
||||||
'deliveries',
|
'deliveries',
|
||||||
'document',
|
'document',
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ const MarketingDetail = ({
|
|||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
router.push('/marketing');
|
router.push('/marketing');
|
||||||
toast.success(res?.message as string);
|
toast.success(res?.message as string);
|
||||||
refresh?.();
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -361,6 +361,8 @@ const MarketingForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const memoSalesOrder = formik.values.sales_order;
|
||||||
|
|
||||||
// ================== FORM REPEATER HANDLER ==================
|
// ================== FORM REPEATER HANDLER ==================
|
||||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -471,13 +473,25 @@ const MarketingForm = ({
|
|||||||
}, [deleteModal]);
|
}, [deleteModal]);
|
||||||
|
|
||||||
// ================== SALES ORDER HANDLER ==================
|
// ================== SALES ORDER HANDLER ==================
|
||||||
const handleDeleteSO = useCallback((id: number) => {
|
const handleDeleteSO = useCallback(
|
||||||
|
(id: number) => {
|
||||||
const currentProducts = formik.values.sales_order;
|
const currentProducts = formik.values.sales_order;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'sales_order',
|
'sales_order',
|
||||||
currentProducts.filter((p) => p.id != id)
|
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 handleBulkDeleteSO = useCallback(() => {
|
||||||
const currentProducts = formik.values.sales_order;
|
const currentProducts = formik.values.sales_order;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
@@ -487,13 +501,13 @@ const MarketingForm = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
setRowSOSelection({});
|
setRowSOSelection({});
|
||||||
}, [selectedRowSOIds]);
|
}, [selectedRowSOIds, memoSalesOrder]);
|
||||||
const handleAddSOClick = useCallback(() => {
|
const handleAddSOClick = useCallback(() => {
|
||||||
setSelectedMarketingProduct(null);
|
setSelectedMarketingProduct(null);
|
||||||
addSOModal.openModal();
|
addSOModal.openModal();
|
||||||
}, [addSOModal]);
|
}, [addSOModal]);
|
||||||
const handleAddSubmitSO = useCallback(
|
const handleAddSubmitSO = useCallback(
|
||||||
async (values: SalesOrderProductFormValues) => {
|
async (values: SalesOrderProductFormValues, id?: number) => {
|
||||||
const currentProducts = formik.values.sales_order;
|
const currentProducts = formik.values.sales_order;
|
||||||
|
|
||||||
const newValues = {
|
const newValues = {
|
||||||
@@ -501,18 +515,12 @@ const MarketingForm = ({
|
|||||||
id: values.id ?? Date.now(),
|
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 = [];
|
let updatedProducts = [];
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
if (id) {
|
||||||
// Overwrite
|
// Overwrite
|
||||||
updatedProducts = currentProducts.map((item, index) =>
|
updatedProducts = currentProducts.map((item) =>
|
||||||
index === existingIndex ? newValues : item
|
item.id === id ? newValues : item
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Add new item
|
// Add new item
|
||||||
@@ -523,7 +531,7 @@ const MarketingForm = ({
|
|||||||
|
|
||||||
addSOModal.closeModal();
|
addSOModal.closeModal();
|
||||||
},
|
},
|
||||||
[addSOModal]
|
[addSOModal, memoSalesOrder]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ================== DELIVERY ORDER HANDLER ==================
|
// ================== DELIVERY ORDER HANDLER ==================
|
||||||
@@ -568,8 +576,30 @@ const MarketingForm = ({
|
|||||||
},
|
},
|
||||||
[addDOModal]
|
[addDOModal]
|
||||||
);
|
);
|
||||||
|
const handleDeleteDO = useCallback(
|
||||||
const memoSalesOrder = formik.values.sales_order;
|
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(() => {
|
useEffect(() => {
|
||||||
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
||||||
@@ -654,6 +684,7 @@ const MarketingForm = ({
|
|||||||
setRowSelection={setRowSOSelection}
|
setRowSelection={setRowSOSelection}
|
||||||
selectedRowIds={selectedRowSOIds}
|
selectedRowIds={selectedRowSOIds}
|
||||||
onDelete={handleDeleteSO}
|
onDelete={handleDeleteSO}
|
||||||
|
onEdit={handleEditSO}
|
||||||
onBulkDelete={handleBulkDeleteSO}
|
onBulkDelete={handleBulkDeleteSO}
|
||||||
onAddProductClick={handleAddSOClick}
|
onAddProductClick={handleAddSOClick}
|
||||||
/>
|
/>
|
||||||
@@ -673,6 +704,7 @@ const MarketingForm = ({
|
|||||||
formType={formType}
|
formType={formType}
|
||||||
data={deliveryOrderValues}
|
data={deliveryOrderValues}
|
||||||
onEdit={handleEditDO}
|
onEdit={handleEditDO}
|
||||||
|
onDelete={handleDeleteDO}
|
||||||
onAddProductClick={handleAddDOClick}
|
onAddProductClick={handleAddDOClick}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
await onUpdateForm?.(values.marketing_product_id as number, values);
|
await onUpdateForm?.(values.marketing_product_id as number, values);
|
||||||
}
|
}
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
|
setSelectedProduct(null);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,7 +125,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
marketing_product: undefined,
|
marketing_product: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setSelectedProduct(null);
|
// setSelectedProduct(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlurField = (field: string) => {
|
const handleBlurField = (field: string) => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type SalesOrderProductSchemaType = {
|
|||||||
avg_weight: string | number | undefined;
|
avg_weight: string | number | undefined;
|
||||||
total_price: string | number | undefined;
|
total_price: string | number | undefined;
|
||||||
vehicle_number?: string | undefined;
|
vehicle_number?: string | undefined;
|
||||||
|
uom?: string | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
||||||
@@ -57,6 +58,7 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
|
|||||||
total_price: Yup.number()
|
total_price: Yup.number()
|
||||||
.min(1, 'Total Penjualan wajib diisi!')
|
.min(1, 'Total Penjualan wajib diisi!')
|
||||||
.required('Total Penjualan wajib diisi!'),
|
.required('Total Penjualan wajib diisi!'),
|
||||||
|
uom: Yup.string().nullable().optional().notRequired(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SalesOrderProductFormValues = Yup.InferType<
|
export type SalesOrderProductFormValues = Yup.InferType<
|
||||||
|
|||||||
+29
-11
@@ -39,7 +39,10 @@ const SalesOrderProductForm = ({
|
|||||||
initialValues?: SalesOrderProductFormValues;
|
initialValues?: SalesOrderProductFormValues;
|
||||||
exisitingValues?: SalesOrderProductFormValues[];
|
exisitingValues?: SalesOrderProductFormValues[];
|
||||||
modalRef?: RefObject<HTMLDialogElement | null>;
|
modalRef?: RefObject<HTMLDialogElement | null>;
|
||||||
onSubmitForm?: (value: SalesOrderProductFormValues) => Promise<void>;
|
onSubmitForm?: (
|
||||||
|
value: SalesOrderProductFormValues,
|
||||||
|
id?: number
|
||||||
|
) => Promise<void>;
|
||||||
}) => {
|
}) => {
|
||||||
const [formErrorMessage, setFormErrorMessage] = useState('');
|
const [formErrorMessage, setFormErrorMessage] = useState('');
|
||||||
const [currentInput, setCurrentInput] = useState<string>('');
|
const [currentInput, setCurrentInput] = useState<string>('');
|
||||||
@@ -61,21 +64,22 @@ const SalesOrderProductForm = ({
|
|||||||
const formik = useFormik<SalesOrderProductFormValues>({
|
const formik = useFormik<SalesOrderProductFormValues>({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
vehicle_number: initialValues?.vehicle_number || undefined,
|
vehicle_number: initialValues?.vehicle_number || '',
|
||||||
kandang_id: initialValues?.kandang_id || undefined,
|
kandang_id: initialValues?.kandang_id || undefined,
|
||||||
kandang: initialValues?.kandang || undefined,
|
kandang: initialValues?.kandang || null,
|
||||||
product_warehouse: initialValues?.product_warehouse || undefined,
|
product_warehouse: initialValues?.product_warehouse || null,
|
||||||
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
||||||
unit_price: initialValues?.unit_price || undefined,
|
unit_price: initialValues?.unit_price || '',
|
||||||
total_weight: initialValues?.total_weight || undefined,
|
total_weight: initialValues?.total_weight || '',
|
||||||
qty: initialValues?.qty || undefined,
|
qty: initialValues?.qty || '',
|
||||||
avg_weight: initialValues?.avg_weight || undefined,
|
avg_weight: initialValues?.avg_weight || '',
|
||||||
total_price: initialValues?.total_price || undefined,
|
total_price: initialValues?.total_price || '',
|
||||||
|
uom: initialValues?.uom || '',
|
||||||
},
|
},
|
||||||
validationSchema: SalesOrderProductSchema,
|
validationSchema: SalesOrderProductSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
setFormErrorMessage('');
|
setFormErrorMessage('');
|
||||||
onSubmitForm?.(values);
|
onSubmitForm?.(values, initialValues?.id);
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
},
|
},
|
||||||
validateOnBlur: true,
|
validateOnBlur: true,
|
||||||
@@ -220,7 +224,19 @@ const SalesOrderProductForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -401,7 +417,9 @@ const SalesOrderProductForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4'>
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-end gap-3 mt-4'>
|
<div className='flex flex-row justify-end gap-3 mt-4'>
|
||||||
<Button type='reset' color='warning' onClick={handleResetForm}>
|
<Button type='reset' color='warning' onClick={handleResetForm}>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type DeliveryOrderProductTableProps = {
|
|||||||
data: DeliveryOrderProductFormValues[];
|
data: DeliveryOrderProductFormValues[];
|
||||||
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
||||||
onEdit: (id: number) => void;
|
onEdit: (id: number) => void;
|
||||||
|
onDelete: (id: number) => void;
|
||||||
onAddProductClick: () => void;
|
onAddProductClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,10 +24,13 @@ const DeliveryOrderProductTable = ({
|
|||||||
data,
|
data,
|
||||||
formType,
|
formType,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
onDelete,
|
||||||
onAddProductClick,
|
onAddProductClick,
|
||||||
}: DeliveryOrderProductTableProps) => {
|
}: DeliveryOrderProductTableProps) => {
|
||||||
const onEditRef = useRef(onEdit);
|
const onEditRef = useRef(onEdit);
|
||||||
onEditRef.current = onEdit;
|
onEditRef.current = onEdit;
|
||||||
|
const onDeleteRef = useRef(onDelete);
|
||||||
|
onDeleteRef.current = onDelete;
|
||||||
|
|
||||||
const canAddData = data.filter((item) => !Boolean(item.qty));
|
const canAddData = data.filter((item) => !Boolean(item.qty));
|
||||||
|
|
||||||
@@ -144,6 +148,7 @@ const DeliveryOrderProductTable = ({
|
|||||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||||
<>
|
<>
|
||||||
{props.row.original.qty && (
|
{props.row.original.qty && (
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
color='warning'
|
color='warning'
|
||||||
className='px-2 py-1 text-sm'
|
className='px-2 py-1 text-sm'
|
||||||
@@ -154,6 +159,18 @@ const DeliveryOrderProductTable = ({
|
|||||||
>
|
>
|
||||||
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
||||||
</Button>
|
</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 && '-'}
|
{!props.row.original.qty && '-'}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type SalesOrderProductTableProps = {
|
|||||||
>;
|
>;
|
||||||
selectedRowIds: number[];
|
selectedRowIds: number[];
|
||||||
onDelete: (id: number) => void;
|
onDelete: (id: number) => void;
|
||||||
|
onEdit: (id: number) => void;
|
||||||
onBulkDelete: () => void;
|
onBulkDelete: () => void;
|
||||||
onAddProductClick: () => void;
|
onAddProductClick: () => void;
|
||||||
};
|
};
|
||||||
@@ -34,11 +35,14 @@ const SalesOrderProductTable = ({
|
|||||||
setRowSelection,
|
setRowSelection,
|
||||||
selectedRowIds,
|
selectedRowIds,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onEdit,
|
||||||
onBulkDelete,
|
onBulkDelete,
|
||||||
onAddProductClick,
|
onAddProductClick,
|
||||||
}: SalesOrderProductTableProps) => {
|
}: SalesOrderProductTableProps) => {
|
||||||
const onDeleteRef = useRef(onDelete);
|
const onDeleteRef = useRef(onDelete);
|
||||||
onDeleteRef.current = onDelete;
|
onDeleteRef.current = onDelete;
|
||||||
|
const onEditRef = useRef(onEdit);
|
||||||
|
onEditRef.current = onEdit;
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -92,17 +96,26 @@ const SalesOrderProductTable = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||||
formatNumber(parseFloat(row.total_weight as string)),
|
formatNumber(parseFloat(row.total_weight as string), undefined, 0, 5),
|
||||||
header: 'Total Bobot (Kg)',
|
header: 'Total Bobot (Kg)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||||
formatNumber(parseFloat(row.qty as string)),
|
formatNumber(parseFloat(row.qty as string)),
|
||||||
header: 'Kuantitas',
|
header: 'Kuantitas',
|
||||||
|
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) =>
|
||||||
|
formatNumber(
|
||||||
|
parseFloat(row.original.qty as string),
|
||||||
|
undefined,
|
||||||
|
0,
|
||||||
|
5
|
||||||
|
) +
|
||||||
|
' ' +
|
||||||
|
(row.original.uom ?? ''),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||||
formatNumber(parseFloat(row.avg_weight as string)),
|
formatNumber(parseFloat(row.avg_weight as string), undefined, 0, 5),
|
||||||
header: 'Avg. Bobot (Kg)',
|
header: 'Avg. Bobot (Kg)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,6 +129,14 @@ const SalesOrderProductTable = ({
|
|||||||
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
|
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
|
||||||
) => (
|
) => (
|
||||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
<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
|
<Button
|
||||||
color='error'
|
color='error'
|
||||||
className='p-1'
|
className='p-1'
|
||||||
@@ -124,7 +145,7 @@ const SalesOrderProductTable = ({
|
|||||||
}
|
}
|
||||||
type='button'
|
type='button'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:trash' width={16} height={16} />
|
<Icon icon='mdi:trash' width={16} height={16} /> Hapus
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
|||||||
import { format } from 'path';
|
import { format } from 'path';
|
||||||
import { date } from 'yup';
|
import { date } from 'yup';
|
||||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
interface DeliveryOrderExportProps {
|
interface DeliveryOrderExportProps {
|
||||||
data?: Marketing;
|
data?: Marketing;
|
||||||
@@ -23,7 +24,7 @@ const DeliveryOrderExport = ({
|
|||||||
|
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!salesData) {
|
if (!salesData) {
|
||||||
alert('No sales order data available');
|
toast.error('No sales order data available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsGeneratingPDF(true);
|
setIsGeneratingPDF(true);
|
||||||
@@ -40,8 +41,7 @@ const DeliveryOrderExport = ({
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating PDF:', error);
|
toast.error('Failed to generate PDF. Please try again.');
|
||||||
alert('Failed to generate PDF. Please try again.');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingPDF(false);
|
setIsGeneratingPDF(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { formatDate, formatNumber } from '@/lib/helper';
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
interface SalesOrderExportProps {
|
interface SalesOrderExportProps {
|
||||||
data?: Marketing;
|
data?: Marketing;
|
||||||
@@ -17,7 +18,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
|||||||
|
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!salesData) {
|
if (!salesData) {
|
||||||
alert('No sales order data available');
|
toast.error('No sales order data available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsGeneratingPDF(true);
|
setIsGeneratingPDF(true);
|
||||||
@@ -32,8 +33,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating PDF:', error);
|
toast.error('Failed to generate PDF. Please try again.');
|
||||||
alert('Failed to generate PDF. Please try again.');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingPDF(false);
|
setIsGeneratingPDF(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -367,7 +367,7 @@ const ProductionStandardForm = ({
|
|||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_hen_house_production,
|
row.production_standard_details?.target_hen_house_production,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
`${row.original.production_standard_details?.target_hen_house_production} pc`,
|
`${row.original.production_standard_details?.target_hen_house_production} btr`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -383,7 +383,7 @@ const ProductionStandardForm = ({
|
|||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_egg_mass,
|
row.production_standard_details?.target_egg_mass,
|
||||||
cell: ({ row }) =>
|
cell: ({ row }) =>
|
||||||
`${row.original.production_standard_details?.target_egg_mass} g`,
|
`${row.original.production_standard_details?.target_egg_mass} kg`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -958,7 +958,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
bottomLabel='Butir (pc)'
|
bottomLabel='Butir (btr)'
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -1015,7 +1015,7 @@ const ProductionStandardForm = ({
|
|||||||
name='production_standard_details.target_egg_mass'
|
name='production_standard_details.target_egg_mass'
|
||||||
label='Egg Mass'
|
label='Egg Mass'
|
||||||
placeholder='1'
|
placeholder='1'
|
||||||
bottomLabel='Gram (g)'
|
bottomLabel='Kg (kg)'
|
||||||
value={
|
value={
|
||||||
repeaterFormik.values
|
repeaterFormik.values
|
||||||
.production_standard_details?.target_egg_mass
|
.production_standard_details?.target_egg_mass
|
||||||
@@ -1176,7 +1176,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
bottomLabel='Gram/Ekor (g)'
|
bottomLabel='Gram (g)'
|
||||||
endAdornment
|
endAdornment
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
|
|||||||
@@ -686,10 +686,18 @@ const RecordingTable = () => {
|
|||||||
1,
|
1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Nama Project',
|
header: 'Lokasi',
|
||||||
|
cell: (props) => props.row.original.location?.name || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Flock',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
props.row.original.project_flock?.flock_name || '-',
|
props.row.original.project_flock?.flock_name || '-',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'Kandang',
|
||||||
|
cell: (props) => props.row.original.kandang?.name || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Periode',
|
header: 'Periode',
|
||||||
cell: (props) => props.row.original.project_flock?.period || '-',
|
cell: (props) => props.row.original.project_flock?.period || '-',
|
||||||
@@ -722,12 +730,6 @@ const RecordingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'warehouse.name',
|
|
||||||
header: 'Gudang',
|
|
||||||
cell: (props) => props.row.original.warehouse?.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'record_date',
|
|
||||||
header: 'Waktu Recording',
|
header: 'Waktu Recording',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'),
|
formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'),
|
||||||
@@ -1011,21 +1013,6 @@ const RecordingTable = () => {
|
|||||||
approvalHistoryModal.openModal();
|
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 (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
@@ -1036,7 +1023,7 @@ const RecordingTable = () => {
|
|||||||
}}
|
}}
|
||||||
onClick={openApprovalHistory}
|
onClick={openApprovalHistory}
|
||||||
>
|
>
|
||||||
{getStatusText(approval.action)}
|
{approval.step_name || approval.action}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,16 +33,16 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
qty: number | string;
|
qty: number | string;
|
||||||
}[];
|
}[];
|
||||||
depletions: {
|
depletions: {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number | string;
|
qty?: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
||||||
eggs: {
|
eggs: {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number | string;
|
qty?: number | string;
|
||||||
weight: number | string;
|
weight?: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,14 +52,14 @@ export type StockSchema = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DepletionSchema = {
|
export type DepletionSchema = {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number | string;
|
qty?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EggSchema = {
|
export type EggSchema = {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number | string;
|
qty?: number | string;
|
||||||
weight: number | string;
|
weight?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
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({
|
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.number()
|
||||||
.required('Produk depletions wajib diisi!')
|
.optional()
|
||||||
.min(1, 'Produk depletions wajib diisi!')
|
.typeError('Depletions harus berupa angka!'),
|
||||||
.typeError('Produk depletions harus berupa angka!'),
|
|
||||||
qty: Yup.number()
|
qty: Yup.number()
|
||||||
.required('Jumlah depletions wajib diisi!')
|
.optional()
|
||||||
.min(1, 'Jumlah depletions minimal 1!')
|
|
||||||
.typeError('Jumlah depletions harus berupa angka!'),
|
.typeError('Jumlah depletions harus berupa angka!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.number()
|
||||||
.required('Kondisi telur wajib diisi!')
|
.optional()
|
||||||
.min(1, 'Kondisi telur wajib diisi!')
|
|
||||||
.typeError('Kondisi telur harus berupa angka!'),
|
.typeError('Kondisi telur harus berupa angka!'),
|
||||||
qty: Yup.number()
|
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
||||||
.required('Jumlah telur wajib diisi!')
|
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
||||||
.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!'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
|
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
|
||||||
@@ -163,18 +154,12 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
|
|||||||
.of(StockObjectSchema)
|
.of(StockObjectSchema)
|
||||||
.min(1, 'Minimal harus ada 1 data stok!')
|
.min(1, 'Minimal harus ada 1 data stok!')
|
||||||
.required('Data stok wajib diisi!'),
|
.required('Data stok wajib diisi!'),
|
||||||
depletions: Yup.array()
|
depletions: Yup.array().of(DepletionObjectSchema).default([]),
|
||||||
.of(DepletionObjectSchema)
|
|
||||||
.min(1, 'Minimal harus ada 1 data depletions!')
|
|
||||||
.required('Data depletions wajib diisi!'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
|
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
|
||||||
RecordingGrowingFormSchema.shape({
|
RecordingGrowingFormSchema.shape({
|
||||||
eggs: Yup.array()
|
eggs: Yup.array().of(EggObjectSchema).default([]),
|
||||||
.of(EggObjectSchema)
|
|
||||||
.min(1, 'Minimal harus ada 1 data telur!')
|
|
||||||
.required('Data telur wajib diisi!'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateRecordingGrowingFormSchema =
|
export const UpdateRecordingGrowingFormSchema =
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ import {
|
|||||||
GROWING_RECORDING_APPROVAL_LINE,
|
GROWING_RECORDING_APPROVAL_LINE,
|
||||||
LAYING_RECORDING_APPROVAL_LINE,
|
LAYING_RECORDING_APPROVAL_LINE,
|
||||||
} from '@/config/approval-line';
|
} from '@/config/approval-line';
|
||||||
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
|
||||||
interface RecordingFormProps {
|
interface RecordingFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -227,7 +228,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const [, setApprovalNotes] = useState('');
|
const [, setApprovalNotes] = useState('');
|
||||||
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
|
const [recordingFormErrorMessage, setRecordingFormErrorMessage] =
|
||||||
useState('');
|
useState('');
|
||||||
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
const [, setNewRecordingData] = useState<Recording | null>(null);
|
const [, setNewRecordingData] = useState<Recording | null>(null);
|
||||||
const [nextDayRecording, setNextDayRecording] =
|
const [nextDayRecording, setNextDayRecording] =
|
||||||
@@ -309,6 +309,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
// ===== PAYLOAD CREATION HELPERS =====
|
// ===== PAYLOAD CREATION HELPERS =====
|
||||||
const createGrowingPayload = useCallback(
|
const createGrowingPayload = useCallback(
|
||||||
(values: RecordingGrowingFormValues) => {
|
(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 {
|
return {
|
||||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||||
record_date: values.record_date,
|
record_date: values.record_date,
|
||||||
@@ -316,10 +323,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
product_warehouse_id: stock.product_warehouse_id,
|
product_warehouse_id: stock.product_warehouse_id,
|
||||||
qty: Number(stock.qty) || 0,
|
qty: Number(stock.qty) || 0,
|
||||||
})),
|
})),
|
||||||
depletions: (values.depletions ?? []).map((depletion) => ({
|
...(depletions && depletions.length > 0 && { depletions }),
|
||||||
product_warehouse_id: depletion.product_warehouse_id,
|
|
||||||
qty: Number(depletion.qty) || 0,
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -327,25 +331,33 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const createLayingPayload = useCallback(
|
const createLayingPayload = useCallback(
|
||||||
(values: RecordingLayingFormValues) => {
|
(values: RecordingLayingFormValues) => {
|
||||||
return {
|
const depletions = values.depletions
|
||||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
?.filter((d) => d.product_warehouse_id && d.qty)
|
||||||
record_date: values.record_date,
|
.map((depletion) => ({
|
||||||
stocks: (values.stocks ?? []).map((stock) => ({
|
product_warehouse_id: depletion.product_warehouse_id!,
|
||||||
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,
|
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,
|
qty: Number(egg.qty) || 0,
|
||||||
weight:
|
weight:
|
||||||
typeof egg.weight === 'number'
|
typeof egg.weight === 'number'
|
||||||
? egg.weight
|
? egg.weight
|
||||||
: parseFloat(String(egg.weight)) || 0,
|
: 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);
|
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')) {
|
if (projectFlockKandangDetail && (type === 'edit' || type === 'detail')) {
|
||||||
baseValues.project_flock_kandang = {
|
baseValues = {
|
||||||
value: projectFlockKandangDetail.project_flock.id,
|
...baseValues,
|
||||||
label: projectFlockKandangDetail.project_flock.flock_name || '',
|
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 =====
|
// ===== HELPER FUNCTIONS =====
|
||||||
const getAvailableStock = useCallback(
|
const getAvailableStock = useCallback(
|
||||||
(productWarehouseId: number) => {
|
(productWarehouseId: number) => {
|
||||||
@@ -1266,6 +1310,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
[formik, duplicateErrorShown]
|
[formik, duplicateErrorShown]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { formErrorList, handleFormSubmit, close } = useFormikErrorList(formik);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectFlockKandangLookup?.project_flock_kandang_id) {
|
if (projectFlockKandangLookup?.project_flock_kandang_id) {
|
||||||
const projectFlockKandangId =
|
const projectFlockKandangId =
|
||||||
@@ -1655,10 +1701,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
{/* Error List Alert */}
|
{/* Error List Alert */}
|
||||||
{formErrorList.length > 0 && (
|
{formErrorList.length > 0 && (
|
||||||
<AlertErrorList
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
formErrorList={formErrorList}
|
|
||||||
onClose={() => setFormErrorList([])}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Basic Info Card */}
|
{/* Basic Info Card */}
|
||||||
@@ -2520,24 +2563,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
<th>
|
<th>Kondisi</th>
|
||||||
Kondisi
|
<th>Jumlah</th>
|
||||||
<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>
|
|
||||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
)}
|
)}
|
||||||
@@ -2615,7 +2642,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
name={`depletions.${idx}.qty`}
|
name={`depletions.${idx}.qty`}
|
||||||
value={depletion.qty ?? ''}
|
value={depletion.qty ?? ''}
|
||||||
onChange={handleDepletionQtyChangeWrapper(idx)}
|
onChange={handleDepletionQtyChangeWrapper(idx)}
|
||||||
@@ -2731,33 +2757,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
/>
|
/>
|
||||||
</th>
|
</th>
|
||||||
)}
|
)}
|
||||||
<th>
|
<th>Kondisi Telur</th>
|
||||||
Kondisi Telur
|
<th>Jumlah</th>
|
||||||
<span
|
<th>Berat (gram)</th>
|
||||||
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>
|
|
||||||
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
{(type as 'add' | 'edit' | 'detail') !== 'detail' && (
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
)}
|
)}
|
||||||
@@ -2792,7 +2794,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
)}
|
)}
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
|
||||||
value={
|
value={
|
||||||
eggProducts.find(
|
eggProducts.find(
|
||||||
(product) =>
|
(product) =>
|
||||||
@@ -2835,7 +2836,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
name={`eggs.${idx}.qty`}
|
name={`eggs.${idx}.qty`}
|
||||||
value={egg.qty ?? ''}
|
value={egg.qty ?? ''}
|
||||||
onChange={handleEggQtyChangeWrapper(idx)}
|
onChange={handleEggQtyChangeWrapper(idx)}
|
||||||
@@ -2860,7 +2860,6 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
name={`eggs.${idx}.weight`}
|
name={`eggs.${idx}.weight`}
|
||||||
value={egg.weight ?? ''}
|
value={egg.weight ?? ''}
|
||||||
onChange={handleEggWeightChangeWrapper(idx)}
|
onChange={handleEggWeightChangeWrapper(idx)}
|
||||||
|
|||||||
@@ -540,31 +540,6 @@ const PurchaseOrderDetail = ({
|
|||||||
accessorKey: 'travel_number',
|
accessorKey: 'travel_number',
|
||||||
cell: (props) => props.row.original.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',
|
header: 'No. Armada Pengangkut',
|
||||||
accessorKey: 'vehicle_number',
|
accessorKey: 'vehicle_number',
|
||||||
@@ -588,7 +563,10 @@ const PurchaseOrderDetail = ({
|
|||||||
{
|
{
|
||||||
header: 'Transport /Item',
|
header: 'Transport /Item',
|
||||||
accessorKey: 'transport_per_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>
|
||||||
<span className='text-gray-900 ml-3 break-all'>
|
<span className='text-gray-900 ml-3 break-all'>
|
||||||
:{' '}
|
:{' '}
|
||||||
{purchaseData.items?.[0]?.warehouse?.type === 'LOKASI' &&
|
{purchaseData.items?.[0]?.warehouse &&
|
||||||
purchaseData.items?.[0]?.warehouse?.location?.name
|
'location' in purchaseData.items[0].warehouse
|
||||||
? purchaseData.items[0].warehouse.location.name
|
? purchaseData.items[0].warehouse.location.name
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
@@ -905,11 +883,29 @@ const PurchaseOrderDetail = ({
|
|||||||
Informasi Penerimaan Barang
|
Informasi Penerimaan Barang
|
||||||
</h3>
|
</h3>
|
||||||
{canShowPenerimaanBarang && (
|
{canShowPenerimaanBarang && (
|
||||||
|
<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>
|
<RowDropdownOptions isLast2Rows>
|
||||||
<PenerimaanBarangDropdown
|
<PenerimaanBarangDropdown
|
||||||
onEdit={penerimaanBarangModal.openModal}
|
onEdit={penerimaanBarangModal.openModal}
|
||||||
/>
|
/>
|
||||||
</RowDropdownOptions>
|
</RowDropdownOptions>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
|
|||||||
@@ -324,12 +324,14 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
|||||||
PT LUMBUNG TELUR INDONESIA
|
PT LUMBUNG TELUR INDONESIA
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
|
{purchaseData?.items?.[0]?.warehouse &&
|
||||||
|
'location' in purchaseData.items[0].warehouse
|
||||||
? purchaseData.items[0].warehouse.location.name
|
? purchaseData.items[0].warehouse.location.name
|
||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
|
{purchaseData?.items?.[0]?.warehouse &&
|
||||||
|
'location' in purchaseData.items[0].warehouse
|
||||||
? purchaseData.items[0].warehouse.location.address
|
? purchaseData.items[0].warehouse.location.address
|
||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -434,7 +436,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
|||||||
</View>
|
</View>
|
||||||
<View style={pdfStyles.tableCell}>
|
<View style={pdfStyles.tableCell}>
|
||||||
<Text>
|
<Text>
|
||||||
{item.warehouse?.type === 'LOKASI'
|
{item.warehouse && 'location' in item.warehouse
|
||||||
? item.warehouse.location.address
|
? item.warehouse.location.address
|
||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<Text>Rata-Rata</Text>
|
<Text>Rata-Rata</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>Harga Awal</Text>
|
<Text>Harga/Unit</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>Harga Akhir</Text>
|
<Text>Harga Akhir</Text>
|
||||||
@@ -378,7 +378,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<Text>{formatNumber(item.average_weight)}</Text>
|
<Text>{formatNumber(item.average_weight)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>{formatCurrency(item.price)}</Text>
|
<Text>{formatCurrency(item.unit_price)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>{formatCurrency(item.final_price)}</Text>
|
<Text>{formatCurrency(item.final_price)}</Text>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const generateCustomerPaymentExcel = (
|
|||||||
'Ekor/Qty': formatNumber(item.qty || 0),
|
'Ekor/Qty': formatNumber(item.qty || 0),
|
||||||
'Berat (Kg)': formatNumber(item.weight || 0),
|
'Berat (Kg)': formatNumber(item.weight || 0),
|
||||||
AVG: formatNumber(item.average_weight || 0),
|
AVG: formatNumber(item.average_weight || 0),
|
||||||
'Harga Awal': formatCurrency(item.price || 0),
|
'Harga/Unit': formatCurrency(item.unit_price || 0),
|
||||||
'Harga Akhir': formatCurrency(item.final_price || 0),
|
'Harga Akhir': formatCurrency(item.final_price || 0),
|
||||||
Total: formatCurrency(item.total_price || 0),
|
Total: formatCurrency(item.total_price || 0),
|
||||||
Pembayaran: formatCurrency(item.payment_amount || 0),
|
Pembayaran: formatCurrency(item.payment_amount || 0),
|
||||||
@@ -62,7 +62,7 @@ export const generateCustomerPaymentExcel = (
|
|||||||
'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0),
|
'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0),
|
||||||
'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0),
|
'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0),
|
||||||
AVG: '',
|
AVG: '',
|
||||||
'Harga Awal': '',
|
'Harga/Unit': '',
|
||||||
'Harga Akhir': formatCurrency(
|
'Harga Akhir': formatCurrency(
|
||||||
customerReport.summary.total_final_amount || 0
|
customerReport.summary.total_final_amount || 0
|
||||||
),
|
),
|
||||||
@@ -89,7 +89,7 @@ export const generateCustomerPaymentExcel = (
|
|||||||
{ wch: 10 }, // Ekor/Qty
|
{ wch: 10 }, // Ekor/Qty
|
||||||
{ wch: 12 }, // Berat
|
{ wch: 12 }, // Berat
|
||||||
{ wch: 10 }, // AVG
|
{ wch: 10 }, // AVG
|
||||||
{ wch: 15 }, // Harga Awal
|
{ wch: 15 }, // Harga/Unit
|
||||||
{ wch: 15 }, // Harga Akhir
|
{ wch: 15 }, // Harga Akhir
|
||||||
{ wch: 15 }, // Total
|
{ wch: 15 }, // Total
|
||||||
{ wch: 15 }, // Pembayaran
|
{ wch: 15 }, // Pembayaran
|
||||||
|
|||||||
@@ -106,7 +106,11 @@ const CustomerPaymentTab = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getPaymentStatusText = (notes: string) => {
|
const getPaymentStatusText = (notes: string) => {
|
||||||
return notes;
|
return notes
|
||||||
|
.toLowerCase()
|
||||||
|
.split(' ')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
@@ -159,7 +163,7 @@ const CustomerPaymentTab = () => {
|
|||||||
isSubmitted
|
isSubmitted
|
||||||
? () => {
|
? () => {
|
||||||
const params = {
|
const params = {
|
||||||
customer_id:
|
customer_ids:
|
||||||
filterCustomer.length > 0
|
filterCustomer.length > 0
|
||||||
? filterCustomer.map((v) => String(v.value)).join(',')
|
? filterCustomer.map((v) => String(v.value)).join(',')
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -180,7 +184,7 @@ const CustomerPaymentTab = () => {
|
|||||||
: null,
|
: null,
|
||||||
([, params]) =>
|
([, params]) =>
|
||||||
FinanceApi.getCustomerPaymentReport(
|
FinanceApi.getCustomerPaymentReport(
|
||||||
params.customer_id,
|
params.customer_ids,
|
||||||
undefined, // TODO: Change to params.sales_id when BE is ready
|
undefined, // TODO: Change to params.sales_id when BE is ready
|
||||||
undefined, // TODO: Change to params.filter_by when BE is ready
|
undefined, // TODO: Change to params.filter_by when BE is ready
|
||||||
params.start_date,
|
params.start_date,
|
||||||
@@ -203,7 +207,7 @@ const CustomerPaymentTab = () => {
|
|||||||
CustomerPaymentReport[] | null
|
CustomerPaymentReport[] | null
|
||||||
> => {
|
> => {
|
||||||
const params = {
|
const params = {
|
||||||
customer_id:
|
customer_ids:
|
||||||
filterCustomer.length > 0
|
filterCustomer.length > 0
|
||||||
? filterCustomer.map((v) => String(v.value)).join(',')
|
? filterCustomer.map((v) => String(v.value)).join(',')
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -219,7 +223,7 @@ const CustomerPaymentTab = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await FinanceApi.getCustomerPaymentReport(
|
const response = await FinanceApi.getCustomerPaymentReport(
|
||||||
params.customer_id,
|
params.customer_ids,
|
||||||
undefined, // TODO: Change to params.sales_id when BE is ready
|
undefined, // TODO: Change to params.sales_id when BE is ready
|
||||||
undefined, // TODO: Change to params.filter_by when BE is ready
|
undefined, // TODO: Change to params.filter_by when BE is ready
|
||||||
params.start_date,
|
params.start_date,
|
||||||
@@ -336,7 +340,9 @@ const CustomerPaymentTab = () => {
|
|||||||
const value = props.row.original.aging_day;
|
const value = props.row.original.aging_day;
|
||||||
return (
|
return (
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
{value && value > 0 ? `${formatNumber(value)} hari` : '-'}
|
{value !== null && value !== undefined
|
||||||
|
? `${formatNumber(value)} hari`
|
||||||
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -405,12 +411,12 @@ const CustomerPaymentTab = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'price',
|
id: 'unit_price',
|
||||||
header: 'Harga Awal',
|
header: 'Harga/Unit',
|
||||||
accessorKey: 'price',
|
accessorKey: 'unit_price',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.price;
|
const value = props.row.original.unit_price;
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
@@ -510,7 +516,7 @@ const CustomerPaymentTab = () => {
|
|||||||
status: getPaymentStatusIndicatorColor(value),
|
status: getPaymentStatusIndicatorColor(value),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{getPaymentStatusText(value)}</span>
|
<span className='capitalize'>{getPaymentStatusText(value)}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -246,7 +246,12 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>HPP Telur (RP/KG)</Text>
|
<Text>HPP Telur (RP/KG)</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeaderRight,
|
||||||
|
{ flex: 1.2, borderRightWidth: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Text>Nominal Sisa</Text>
|
<Text>Nominal Sisa</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -301,7 +306,12 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text>
|
<Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellRight,
|
||||||
|
{ flex: 1.2, borderRightWidth: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Text>{formatCurrency(group.egg_value_rp)}</Text>
|
<Text>{formatCurrency(group.egg_value_rp)}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -347,7 +357,12 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
<Text>HPP Telur (RP/KG)</Text>
|
<Text>HPP Telur (RP/KG)</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeaderRight,
|
||||||
|
{ flex: 1.2, borderRightWidth: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Text>Nominal Sisa</Text>
|
<Text>Nominal Sisa</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -356,12 +371,7 @@ const createPDFDocument = (
|
|||||||
{data.rows.map((item: HppPerKandangRow, index: number) => (
|
{data.rows.map((item: HppPerKandangRow, index: number) => (
|
||||||
<View
|
<View
|
||||||
key={index}
|
key={index}
|
||||||
style={[
|
style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}
|
||||||
pdfStyles.tableRow,
|
|
||||||
index < data.rows.length - 1
|
|
||||||
? pdfStyles.tableBorderBottom
|
|
||||||
: {},
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
|
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
|
||||||
<Text>{index + 1}</Text>
|
<Text>{index + 1}</Text>
|
||||||
@@ -410,11 +420,199 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
<Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text>
|
<Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellRight,
|
||||||
|
{ flex: 1.2, borderRightWidth: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Text>{formatCurrency(item.egg_value_rp)}</Text>
|
<Text>{formatCurrency(item.egg_value_rp)}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* TOTAL Row */}
|
||||||
|
{data.summary?.total && (
|
||||||
|
<View style={pdfStyles.tableRow}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeader,
|
||||||
|
{
|
||||||
|
flex: 0.5,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>TOTAL</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeader,
|
||||||
|
{
|
||||||
|
flex: 1.5,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>ALL</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeader,
|
||||||
|
{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>-</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeaderRight,
|
||||||
|
{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{formatNumber(data.summary.total.average_weight_kg)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeaderRight,
|
||||||
|
{
|
||||||
|
flex: 0.8,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{formatNumber(
|
||||||
|
data.summary.total.total_egg_production_pieces
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeaderRight,
|
||||||
|
{
|
||||||
|
flex: 0.8,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{formatNumber(data.summary.total.total_egg_production_kg)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeader,
|
||||||
|
{
|
||||||
|
flex: 1.2,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{data.rows
|
||||||
|
.flatMap((row: HppPerKandangRow) =>
|
||||||
|
row.feed_suppliers?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(v: string, i: number, a: string[]) =>
|
||||||
|
a.indexOf(v) === i
|
||||||
|
)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeader,
|
||||||
|
{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{data.rows
|
||||||
|
.flatMap((row: HppPerKandangRow) =>
|
||||||
|
row.doc_suppliers?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(v: string, i: number, a: string[]) =>
|
||||||
|
a.indexOf(v) === i
|
||||||
|
)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeaderRight,
|
||||||
|
{
|
||||||
|
flex: 1.2,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{formatCurrency(
|
||||||
|
data.summary.total.total_average_doc_price_rp
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeaderRight,
|
||||||
|
{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{formatCurrency(
|
||||||
|
data.summary.total.average_egg_hpp_rp_per_kg
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellHeaderRight,
|
||||||
|
{
|
||||||
|
flex: 1.2,
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
borderRightWidth: 0,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{formatCurrency(data.summary.total.total_egg_value_rp)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import DateInput from '@/components/input/DateInput';
|
|||||||
import NumberInput from '@/components/input/NumberInput';
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { LocationApi } 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 { SaleReportApi } from '@/services/api/report/marketing-sale';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { ColumnDef, Row, flexRender } from '@tanstack/react-table';
|
import { ColumnDef, Row, flexRender } from '@tanstack/react-table';
|
||||||
@@ -40,6 +40,9 @@ const HppPerKandangTab = () => {
|
|||||||
// ===== SUBMISSION STATE =====
|
// ===== SUBMISSION STATE =====
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
|
// ===== VALIDATION STATE =====
|
||||||
|
const [weightMaxError, setWeightMaxError] = useState<string>('');
|
||||||
|
|
||||||
// ===== TABLE FILTER STATE =====
|
// ===== TABLE FILTER STATE =====
|
||||||
const { state: tableFilterState, updateFilter } = useTableFilter({
|
const { state: tableFilterState, updateFilter } = useTableFilter({
|
||||||
initial: {
|
initial: {
|
||||||
@@ -77,7 +80,12 @@ const HppPerKandangTab = () => {
|
|||||||
options: kandangOptions,
|
options: kandangOptions,
|
||||||
isLoadingOptions: isLoadingKandangs,
|
isLoadingOptions: isLoadingKandangs,
|
||||||
loadMore: loadMoreKandangs,
|
loadMore: loadMoreKandangs,
|
||||||
} = useSelect(KandangApi.basePath, 'id', 'name', 'search');
|
} = useSelect(
|
||||||
|
ProjectFlockKandangApi.basePath,
|
||||||
|
'id',
|
||||||
|
'name_with_period',
|
||||||
|
'search'
|
||||||
|
);
|
||||||
|
|
||||||
const showUnrecordedOptions: OptionType[] = [
|
const showUnrecordedOptions: OptionType[] = [
|
||||||
{ value: 'false', label: 'Sembunyikan' },
|
{ value: 'false', label: 'Sembunyikan' },
|
||||||
@@ -127,8 +135,12 @@ const HppPerKandangTab = () => {
|
|||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
updateFilter('weight_min', val ? String(parseFloat(val) || 0) : '');
|
updateFilter('weight_min', val ? String(parseFloat(val) || 0) : '');
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
|
|
||||||
|
if (weightMaxError) {
|
||||||
|
setWeightMaxError('');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[updateFilter]
|
[updateFilter, weightMaxError]
|
||||||
);
|
);
|
||||||
|
|
||||||
const weightMaxChangeHandler = useCallback<
|
const weightMaxChangeHandler = useCallback<
|
||||||
@@ -136,10 +148,22 @@ const HppPerKandangTab = () => {
|
|||||||
>(
|
>(
|
||||||
(e) => {
|
(e) => {
|
||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
updateFilter('weight_max', val ? String(parseFloat(val) || 0) : '');
|
const weightMax = val ? parseFloat(val) || 0 : 0;
|
||||||
|
const weightMin = tableFilterState.weight_min
|
||||||
|
? parseFloat(tableFilterState.weight_min)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (weightMax < weightMin) {
|
||||||
|
setWeightMaxError('Rentang bobot max tidak boleh lebih kecil dari min');
|
||||||
|
toast.error('Rentang bobot max tidak boleh lebih kecil dari min');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWeightMaxError('');
|
||||||
|
updateFilter('weight_max', val ? String(weightMax) : '');
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
},
|
},
|
||||||
[updateFilter]
|
[updateFilter, tableFilterState.weight_min]
|
||||||
);
|
);
|
||||||
|
|
||||||
const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
@@ -325,8 +349,53 @@ const HppPerKandangTab = () => {
|
|||||||
const allExportData =
|
const allExportData =
|
||||||
allDataForExport.rows as HppPerKandangReport['rows'];
|
allDataForExport.rows as HppPerKandangReport['rows'];
|
||||||
|
|
||||||
|
const perWeightRangeSummary =
|
||||||
|
allDataForExport.summary.per_weight_range || [];
|
||||||
|
|
||||||
const summaryTotal = allDataForExport.summary.total;
|
const summaryTotal = allDataForExport.summary.total;
|
||||||
|
|
||||||
|
const rekapitulasiData: { [key: string]: string | number }[] =
|
||||||
|
perWeightRangeSummary.map(
|
||||||
|
(item: HppPerKandangPerWeightRange, index: number) => ({
|
||||||
|
No: index + 1,
|
||||||
|
'Rentang BW': item.label || '',
|
||||||
|
'Sisa Butir': item.egg_production_pieces || 0,
|
||||||
|
'Sisa Kg': item.egg_production_kg || 0,
|
||||||
|
'Rata-Rata Bobot (Kg)': item.avg_weight_kg || 0,
|
||||||
|
'Feed (Supplier)':
|
||||||
|
item.feed_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) => s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ') || '',
|
||||||
|
'DOC (Supplier)':
|
||||||
|
item.doc_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) => s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ') || '',
|
||||||
|
'Rata-Rata Harga DOC': item.average_doc_price_rp || 0,
|
||||||
|
'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0,
|
||||||
|
'Nominal Sisa': item.egg_value_rp || 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const rekapitulasiWorksheet = XLSX.utils.json_to_sheet(rekapitulasiData);
|
||||||
|
|
||||||
|
const rekapitulasiColWidths = [
|
||||||
|
{ wch: 5 }, // No
|
||||||
|
{ wch: 15 }, // Rentang BW
|
||||||
|
{ wch: 15 }, // Sisa Butir
|
||||||
|
{ wch: 12 }, // Sisa Kg
|
||||||
|
{ wch: 18 }, // Rata-Rata Bobot (Kg)
|
||||||
|
{ wch: 20 }, // Feed (Supplier)
|
||||||
|
{ wch: 20 }, // DOC (Supplier)
|
||||||
|
{ wch: 20 }, // Rata-Rata Harga DOC
|
||||||
|
{ wch: 18 }, // HPP Telur (RP/KG)
|
||||||
|
{ wch: 25 }, // Nominal Sisa
|
||||||
|
];
|
||||||
|
rekapitulasiWorksheet['!cols'] = rekapitulasiColWidths;
|
||||||
|
|
||||||
const excelData: { [key: string]: string | number }[] = allExportData.map(
|
const excelData: { [key: string]: string | number }[] = allExportData.map(
|
||||||
(item: HppPerKandangRow, index: number) => ({
|
(item: HppPerKandangRow, index: number) => ({
|
||||||
No: index + 1,
|
No: index + 1,
|
||||||
@@ -384,7 +453,12 @@ const HppPerKandangTab = () => {
|
|||||||
worksheet['!cols'] = colWidths;
|
worksheet['!cols'] = colWidths;
|
||||||
|
|
||||||
const workbook = XLSX.utils.book_new();
|
const workbook = XLSX.utils.book_new();
|
||||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang');
|
XLSX.utils.book_append_sheet(
|
||||||
|
workbook,
|
||||||
|
rekapitulasiWorksheet,
|
||||||
|
'Rekapitulasi'
|
||||||
|
);
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Detail Per Kandang');
|
||||||
|
|
||||||
const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`;
|
const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`;
|
||||||
|
|
||||||
@@ -488,8 +562,8 @@ const HppPerKandangTab = () => {
|
|||||||
header: 'Kandang',
|
header: 'Kandang',
|
||||||
accessorKey: 'kandang.name',
|
accessorKey: 'kandang.name',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const kandang = props.row.original.kandang;
|
const row = props.row.original;
|
||||||
return kandang?.name || '-';
|
return row.name_with_periode || row.kandang?.name || '-';
|
||||||
},
|
},
|
||||||
footer: () => <div className='font-semibold text-gray-900'>ALL</div>,
|
footer: () => <div className='font-semibold text-gray-900'>ALL</div>,
|
||||||
},
|
},
|
||||||
@@ -741,6 +815,8 @@ const HppPerKandangTab = () => {
|
|||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
onMenuScrollToBottom={loadMoreAreas}
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
isLoading={isLoadingAreas}
|
isLoading={isLoadingAreas}
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
hideSelectedOptions={false}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -757,6 +833,8 @@ const HppPerKandangTab = () => {
|
|||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
onMenuScrollToBottom={loadMoreLocations}
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isLoading={isLoadingLocations}
|
isLoading={isLoadingLocations}
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
hideSelectedOptions={false}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -773,6 +851,8 @@ const HppPerKandangTab = () => {
|
|||||||
onInputChange={setKandangInputValue}
|
onInputChange={setKandangInputValue}
|
||||||
onMenuScrollToBottom={loadMoreKandangs}
|
onMenuScrollToBottom={loadMoreKandangs}
|
||||||
isLoading={isLoadingKandangs}
|
isLoading={isLoadingKandangs}
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
hideSelectedOptions={false}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -792,6 +872,8 @@ const HppPerKandangTab = () => {
|
|||||||
placeholder='Masukkan bobot maximum'
|
placeholder='Masukkan bobot maximum'
|
||||||
value={tableFilterState.weight_max}
|
value={tableFilterState.weight_max}
|
||||||
onChange={weightMaxChangeHandler}
|
onChange={weightMaxChangeHandler}
|
||||||
|
isError={!!weightMaxError}
|
||||||
|
errorMessage={weightMaxError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DateInput
|
<DateInput
|
||||||
@@ -818,7 +900,11 @@ const HppPerKandangTab = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-4 flex justify-end gap-2 [&_button]:px-4'>
|
<div className='mt-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||||
<Button color='primary' onClick={handleSubmit}>
|
<Button
|
||||||
|
color='primary'
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={!!weightMaxError}
|
||||||
|
>
|
||||||
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||||
Cari
|
Cari
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -74,23 +74,7 @@ export const RECORDING_APPROVAL_LINE: ApprovalLine = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
step_number: 2,
|
step_number: 2,
|
||||||
step_name: 'Approval Head Area',
|
step_name: 'Disetujui',
|
||||||
},
|
|
||||||
{
|
|
||||||
step_number: 3,
|
|
||||||
step_name: 'Approval Business Unit Vice President',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step_number: 4,
|
|
||||||
step_name: 'Approval Finance',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step_number: 5,
|
|
||||||
step_name: 'Realisasi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
step_number: 6,
|
|
||||||
step_name: 'Selesai',
|
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -389,6 +389,14 @@ export const FINANCE_INITIAL_BALANCE_TYPE_OPTIONS = [
|
|||||||
{ label: 'Saldo Awal Negatif', value: 'NEGATIVE' },
|
{ 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_TRANSACTION_STATUS = ['PENJUALAN', 'PEMBELIAN', 'BIAYA'];
|
||||||
|
|
||||||
export const FINANCE_INITIAL_BALANCE_STATUS = ['SALDO_AWAL'];
|
export const FINANCE_INITIAL_BALANCE_STATUS = ['SALDO_AWAL'];
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
ClosingSapronakCalculation,
|
ClosingSapronakCalculation,
|
||||||
ClosingProductionData,
|
ClosingProductionData,
|
||||||
ClosingHppExpedition,
|
ClosingHppExpedition,
|
||||||
|
ClosingIncomingSapronakSummary,
|
||||||
|
ClosingOutgoingSapronakSummary,
|
||||||
} from '@/types/api/closing';
|
} from '@/types/api/closing';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
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(
|
async getAllOutgoingSapronakFetcher(
|
||||||
endpoint: string
|
endpoint: string
|
||||||
): Promise<BaseApiResponse<ClosingOutgoingSapronak[]>> {
|
): 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(
|
async getGeneralInfo(
|
||||||
id: number
|
id: number
|
||||||
): Promise<BaseApiResponse<ClosingGeneralInformation> | undefined> {
|
): Promise<BaseApiResponse<ClosingGeneralInformation> | undefined> {
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ export class SalesOrderService extends BaseApiService<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error approve marketing:', error);
|
throw error;
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +71,7 @@ export class SalesOrderService extends BaseApiService<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error bulk approve marketing:', error);
|
throw error;
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +93,7 @@ export class SalesOrderService extends BaseApiService<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error delivery marketing:', error);
|
throw error;
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ export class ChickinService extends BaseApiService<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error approve chickin:', error);
|
throw error;
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class FinanceApiService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCustomerPaymentReport(
|
async getCustomerPaymentReport(
|
||||||
customer_id?: string,
|
customer_ids?: string,
|
||||||
// TODO: Uncomment when BE is ready
|
// TODO: Uncomment when BE is ready
|
||||||
// sales_id?: string,
|
// sales_id?: string,
|
||||||
// filter_by?: 'do_date',
|
// filter_by?: 'do_date',
|
||||||
@@ -28,7 +28,7 @@ export class FinanceApiService extends BaseApiService<
|
|||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: {
|
params: {
|
||||||
customer_id: customer_id,
|
customer_ids: customer_ids,
|
||||||
// TODO: Uncomment when BE is ready
|
// TODO: Uncomment when BE is ready
|
||||||
// sales_id: sales_id,
|
// sales_id: sales_id,
|
||||||
// filter_by: filter_by,
|
// filter_by: filter_by,
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ export const createDashboardFilterSlice: StateCreator<
|
|||||||
setFilterValues: (values) => set({ filterValues: values }),
|
setFilterValues: (values) => set({ filterValues: values }),
|
||||||
|
|
||||||
resetFilterValues: () => {
|
resetFilterValues: () => {
|
||||||
alert('reset filter values');
|
|
||||||
|
|
||||||
return set({
|
return set({
|
||||||
filterValues: {
|
filterValues: {
|
||||||
startDate: '',
|
startDate: '',
|
||||||
|
|||||||
Vendored
+9
@@ -11,6 +11,7 @@ import { Product } from '@type/api/master-data/product';
|
|||||||
import { Customer } from '@type/api/master-data/customer';
|
import { Customer } from '@type/api/master-data/customer';
|
||||||
import { BaseMetadata } from '@/types/api/api-general';
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||||
|
import { BaseUom } from '@/types/api/master-data/uom';
|
||||||
|
|
||||||
export type BaseSales = {
|
export type BaseSales = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -104,8 +105,16 @@ export type ClosingIncomingSapronak = {
|
|||||||
notes: string;
|
notes: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ClosingIncomingSapronakSummary = {
|
||||||
|
category: string;
|
||||||
|
total_qty: number;
|
||||||
|
uom: BaseUom;
|
||||||
|
};
|
||||||
|
|
||||||
export type ClosingOutgoingSapronak = ClosingIncomingSapronak;
|
export type ClosingOutgoingSapronak = ClosingIncomingSapronak;
|
||||||
|
|
||||||
|
export type ClosingOutgoingSapronakSummary = ClosingIncomingSapronakSummary;
|
||||||
|
|
||||||
export type ClosingProductionData = {
|
export type ClosingProductionData = {
|
||||||
purchase: {
|
purchase: {
|
||||||
initial_population: number;
|
initial_population: number;
|
||||||
|
|||||||
Vendored
+2
-2
@@ -34,7 +34,7 @@ export type BaseExpense = {
|
|||||||
nonstock_id: number;
|
nonstock_id: number;
|
||||||
qty: number;
|
qty: number;
|
||||||
price: number;
|
price: number;
|
||||||
note?: string;
|
notes?: string;
|
||||||
nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>;
|
nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}[];
|
}[];
|
||||||
@@ -43,7 +43,7 @@ export type BaseExpense = {
|
|||||||
expense_nonstock_id: number;
|
expense_nonstock_id: number;
|
||||||
qty: number;
|
qty: number;
|
||||||
price: number;
|
price: number;
|
||||||
note?: string;
|
notes?: string;
|
||||||
nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>;
|
nonstock: Pick<BaseNonstock, 'id' | 'name' | 'flags'>;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}[];
|
}[];
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type BaseProjectFlockKandang = {
|
|||||||
kandang_id: number;
|
kandang_id: number;
|
||||||
kandang: Kandang;
|
kandang: Kandang;
|
||||||
project_flock: ProjectFlock;
|
project_flock: ProjectFlock;
|
||||||
|
name_with_period?: string;
|
||||||
approval: BaseApproval;
|
approval: BaseApproval;
|
||||||
chickins?: Chickin[];
|
chickins?: Chickin[];
|
||||||
available_qtys?: AvailableQty[];
|
available_qtys?: AvailableQty[];
|
||||||
|
|||||||
+9
-5
@@ -1,6 +1,8 @@
|
|||||||
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
|
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { Location } from '@/types/api/master-data/location';
|
||||||
|
|
||||||
export type ProductionStandard = {
|
export type ProductionStandard = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -87,6 +89,8 @@ export type Recording = BaseMetadata &
|
|||||||
approval?: BaseApproval;
|
approval?: BaseApproval;
|
||||||
created_user: User;
|
created_user: User;
|
||||||
warehouse?: Warehouse;
|
warehouse?: Warehouse;
|
||||||
|
kandang?: Kandang;
|
||||||
|
location?: Location;
|
||||||
product_category?: 'GROWING' | 'LAYING';
|
product_category?: 'GROWING' | 'LAYING';
|
||||||
depletions?: RecordingDepletion[];
|
depletions?: RecordingDepletion[];
|
||||||
stocks?: RecordingStock[];
|
stocks?: RecordingStock[];
|
||||||
@@ -107,15 +111,15 @@ export type CreateGrowingRecordingPayload = {
|
|||||||
qty: number;
|
qty: number;
|
||||||
}[];
|
}[];
|
||||||
depletions?: {
|
depletions?: {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number;
|
qty?: number;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateEggPayload = {
|
export type CreateEggPayload = {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number;
|
qty?: number;
|
||||||
weight: number;
|
weight?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
|
export type CreateLayingRecordingPayload = CreateGrowingRecordingPayload & {
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ export type CustomerPaymentRow = {
|
|||||||
qty: number;
|
qty: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
average_weight: number;
|
average_weight: number;
|
||||||
price: number;
|
unit_price: number;
|
||||||
final_price: number;
|
final_price: number;
|
||||||
total_price: number;
|
total_price: number;
|
||||||
payment_amount: number;
|
payment_amount: number;
|
||||||
|
|||||||
+1
@@ -5,6 +5,7 @@ import { Kandang } from '@/types/api/master-data/kandang';
|
|||||||
export type HppPerKandangRow = {
|
export type HppPerKandangRow = {
|
||||||
id: number;
|
id: number;
|
||||||
kandang: Kandang;
|
kandang: Kandang;
|
||||||
|
name_with_periode?: string;
|
||||||
weight_range: {
|
weight_range: {
|
||||||
weight_min: number;
|
weight_min: number;
|
||||||
weight_max: number;
|
weight_max: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user