mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +00:00
Merge branch 'development' into 'staging'
Development See merge request mbugroup/lti-web-client!215
This commit is contained in:
Generated
+7
@@ -17,6 +17,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"jspdf": "^3.0.4",
|
"jspdf": "^3.0.4",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
@@ -7380,6 +7381,12 @@
|
|||||||
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
|
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/html-to-image": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/html2canvas": {
|
"node_modules/html2canvas": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"jspdf": "^3.0.4",
|
"jspdf": "^3.0.4",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -8,19 +8,22 @@ import SelectInput, {
|
|||||||
OptionType,
|
OptionType,
|
||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { DashboardApi } from '@/services/api/dashboard';
|
import { DashboardApi } from '@/services/api/dashboard';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
|
import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF';
|
||||||
import {
|
import {
|
||||||
DashboardFilterType,
|
DashboardFilterType,
|
||||||
getDashboardFilterSchema,
|
getDashboardFilterSchema,
|
||||||
} from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
} from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
||||||
import DashboardLineChart from '@/components/pages/dashboard/chart/DashboardLineChart';
|
import DashboardLineChart from '@/components/pages/dashboard/chart/DashboardLineChart';
|
||||||
import DashboardLineChartSkeleton from '@/components/pages/dashboard/skeleton/DashboardLineChartSkeleton';
|
import DashboardLineChartSkeleton from '@/components/pages/dashboard/skeleton/DashboardLineChartSkeleton';
|
||||||
|
import DashboardAllCharts, {
|
||||||
|
DashboardAllChartsRef,
|
||||||
|
} from '@/components/pages/dashboard/chart/DashboardAllCharts';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
||||||
import {
|
import {
|
||||||
DashboardFilter,
|
DashboardFilter,
|
||||||
@@ -31,10 +34,10 @@ import { isResponseSuccess } from '@/lib/api-helper';
|
|||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
|
import { useDashboardStore } from '@/stores/dashboard';
|
||||||
|
|
||||||
// Helper function to normalize values to array
|
// Helper function to normalize values to array
|
||||||
const normalizeToArray = (
|
const normalizeToArray = (
|
||||||
@@ -49,12 +52,22 @@ const normalizeToArray = (
|
|||||||
|
|
||||||
const DashboardProduction = () => {
|
const DashboardProduction = () => {
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== DASHBOARD STORE =====
|
||||||
|
const { filterValues, setFilterValues, resetFilterValues } =
|
||||||
|
useDashboardStore();
|
||||||
|
|
||||||
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
||||||
'OVERVIEW'
|
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
|
||||||
);
|
);
|
||||||
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
|
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
|
||||||
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>([]);
|
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>(
|
||||||
|
normalizeToArray(filterValues.location)
|
||||||
|
);
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
|
const statsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const allChartsRef = useRef<DashboardAllChartsRef>(null);
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -105,19 +118,22 @@ const DashboardProduction = () => {
|
|||||||
// ===== FORMIK =====
|
// ===== FORMIK =====
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
startDate: '',
|
startDate: filterValues.startDate || '',
|
||||||
endDate: '',
|
endDate: filterValues.endDate || '',
|
||||||
flock: [] as OptionType[],
|
flock: filterValues.flock || ([] as OptionType[]),
|
||||||
location: [] as OptionType[],
|
location: filterValues.location || ([] as OptionType[]),
|
||||||
kandang: [] as OptionType[],
|
kandang: filterValues.kandang || ([] as OptionType[]),
|
||||||
analysisMode: analysisMode,
|
analysisMode: filterValues.analysisMode || analysisMode,
|
||||||
comparisonType: '',
|
comparisonType: filterValues.comparisonType || '',
|
||||||
lokasiIds: [],
|
locationIds: filterValues.locationIds || [],
|
||||||
flockIds: [],
|
flockIds: filterValues.flockIds || [],
|
||||||
kandangIds: [],
|
kandangIds: filterValues.kandangIds || [],
|
||||||
} as DashboardFilterType,
|
} as DashboardFilterType,
|
||||||
validationSchema: getDashboardFilterSchema(analysisMode),
|
validationSchema: getDashboardFilterSchema(analysisMode),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
|
// Save filter values to store
|
||||||
|
setFilterValues(values);
|
||||||
|
|
||||||
handleApplyFilter({
|
handleApplyFilter({
|
||||||
start_date: values.startDate || '',
|
start_date: values.startDate || '',
|
||||||
end_date: values.endDate || '',
|
end_date: values.endDate || '',
|
||||||
@@ -132,8 +148,10 @@ const DashboardProduction = () => {
|
|||||||
|
|
||||||
const handleResetFilter = () => {
|
const handleResetFilter = () => {
|
||||||
formik.resetForm();
|
formik.resetForm();
|
||||||
|
resetFilterValues(); // Clear stored filter values
|
||||||
setAnalysisMode('OVERVIEW');
|
setAnalysisMode('OVERVIEW');
|
||||||
setEndpointUrl('/dashboards');
|
setEndpointUrl('/dashboards');
|
||||||
|
setSelectedLocationIds([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApplyFilter = (values: DashboardFilter) => {
|
const handleApplyFilter = (values: DashboardFilter) => {
|
||||||
@@ -156,25 +174,33 @@ const DashboardProduction = () => {
|
|||||||
refreshDashboardProductionData();
|
refreshDashboardProductionData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== Load filter from store on mount =====
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filterValues) return;
|
||||||
|
handleApplyFilter({
|
||||||
|
start_date: filterValues.startDate,
|
||||||
|
end_date: filterValues.endDate,
|
||||||
|
analysis_mode: filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON',
|
||||||
|
location_ids: normalizeToArray(filterValues.location),
|
||||||
|
flock_ids: normalizeToArray(filterValues.flock),
|
||||||
|
kandang_ids: normalizeToArray(filterValues.kandang),
|
||||||
|
comparison_type: filterValues.comparisonType,
|
||||||
|
});
|
||||||
|
}, [filterValues]);
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||||
|
|
||||||
// ===== Export PDF =====
|
// ===== Export PDF =====
|
||||||
const handleExportPDF = () => {
|
const handleExportPDF = async () => {
|
||||||
setExporting(true);
|
await generateDashboardPDF({
|
||||||
|
filterValues: formik.values,
|
||||||
|
statsRef,
|
||||||
|
allChartsRef,
|
||||||
|
setExporting,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wait for state to render, then trigger print
|
|
||||||
useEffect(() => {
|
|
||||||
if (exporting) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
window.print();
|
|
||||||
setExporting(false);
|
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [exporting]);
|
|
||||||
|
|
||||||
if (isLoadingDashboardProductionData) {
|
if (isLoadingDashboardProductionData) {
|
||||||
return (
|
return (
|
||||||
<div className='w-full min-h-screen flex items-center justify-center'>
|
<div className='w-full min-h-screen flex items-center justify-center'>
|
||||||
@@ -219,34 +245,71 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dashboard Stats */}
|
{/* Dashboard Stats */}
|
||||||
<DashboardStats data={dashboardProductionData?.statistics_data ?? []} />
|
<div ref={statsRef}>
|
||||||
|
<DashboardStats
|
||||||
|
data={dashboardProductionData?.statistics_data ?? []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Use DashboardLineChart component or skeleton */}
|
{/* Use DashboardLineChart component or skeleton */}
|
||||||
{isLoadingDashboardProductionData ? (
|
<div ref={chartRef}>
|
||||||
<DashboardLineChartSkeleton />
|
{isLoadingDashboardProductionData ? (
|
||||||
) : dashboardProductionData &&
|
<DashboardLineChartSkeleton />
|
||||||
dashboardProductionData.charts &&
|
) : dashboardProductionData &&
|
||||||
Object.keys(dashboardProductionData.charts).length > 0 ? (
|
dashboardProductionData.charts &&
|
||||||
<DashboardLineChart
|
Object.keys(dashboardProductionData.charts).length > 0 ? (
|
||||||
analysisMode={
|
<DashboardLineChart
|
||||||
isResponseSuccess(dashboardProductionResponse)
|
analysisMode={
|
||||||
? dashboardProductionResponse.meta
|
isResponseSuccess(dashboardProductionResponse)
|
||||||
? (
|
? dashboardProductionResponse.meta
|
||||||
dashboardProductionResponse.meta as unknown as DashboardMeta
|
? (
|
||||||
).filters?.analysis_mode
|
dashboardProductionResponse.meta as unknown as DashboardMeta
|
||||||
|
).filters?.analysis_mode
|
||||||
|
: analysisMode
|
||||||
: analysisMode
|
: analysisMode
|
||||||
: analysisMode
|
}
|
||||||
}
|
data={dashboardProductionData}
|
||||||
data={dashboardProductionData}
|
selectedKandang={
|
||||||
/>
|
analysisMode === 'OVERVIEW'
|
||||||
) : (
|
? (formik.values.kandang as OptionType)
|
||||||
<DashboardLineChartSkeleton
|
: undefined
|
||||||
meta={
|
}
|
||||||
isResponseSuccess(dashboardProductionResponse)
|
/>
|
||||||
? (dashboardProductionResponse.meta as unknown as DashboardMeta)
|
) : (
|
||||||
: undefined
|
<DashboardLineChartSkeleton
|
||||||
}
|
meta={
|
||||||
/>
|
isResponseSuccess(dashboardProductionResponse)
|
||||||
|
? (dashboardProductionResponse.meta as unknown as DashboardMeta)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */}
|
||||||
|
{dashboardProductionData && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '-9999px',
|
||||||
|
top: 0,
|
||||||
|
width: '1200px', // Fixed width for consistent PDF rendering
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DashboardAllCharts
|
||||||
|
ref={allChartsRef}
|
||||||
|
data={dashboardProductionData}
|
||||||
|
analysisMode={
|
||||||
|
isResponseSuccess(dashboardProductionResponse)
|
||||||
|
? dashboardProductionResponse.meta
|
||||||
|
? (
|
||||||
|
dashboardProductionResponse.meta as unknown as DashboardMeta
|
||||||
|
).filters?.analysis_mode
|
||||||
|
: analysisMode
|
||||||
|
: analysisMode
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -475,7 +538,6 @@ const DashboardProduction = () => {
|
|||||||
type='reset'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='ms-4 min-w-36 rounded-lg'
|
className='ms-4 min-w-36 rounded-lg'
|
||||||
onClick={handleResetFilter}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import Card from '@/components/Card';
|
||||||
|
import {
|
||||||
|
Dashboard,
|
||||||
|
DashboardOverviewCharts,
|
||||||
|
DashboardComparisonCharts,
|
||||||
|
DashboardChartsSeries,
|
||||||
|
DashboardChartsDataset,
|
||||||
|
} from '@/types/api/dashboard/dashboard';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
CartesianGrid,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
|
type DashboardAllChartsProps = {
|
||||||
|
data: Dashboard;
|
||||||
|
analysisMode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardAllChartsRef = {
|
||||||
|
getChartRefs: () => {
|
||||||
|
key: string;
|
||||||
|
ref: HTMLDivElement | null;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type guard to check if charts is DashboardOverviewCharts
|
||||||
|
function isOverviewCharts(
|
||||||
|
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||||
|
): charts is DashboardOverviewCharts {
|
||||||
|
if (!charts) return false;
|
||||||
|
return (
|
||||||
|
'deplesi' in charts ||
|
||||||
|
'body_weight' in charts ||
|
||||||
|
'fcr' in charts ||
|
||||||
|
'performance' in charts ||
|
||||||
|
'quality_control' in charts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard to check if charts is DashboardComparisonCharts
|
||||||
|
function isComparisonCharts(
|
||||||
|
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||||
|
): charts is DashboardComparisonCharts {
|
||||||
|
if (!charts) return false;
|
||||||
|
return 'farm' in charts || 'flock' in charts || 'kandang' in charts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineColors: Record<string, string> = {
|
||||||
|
body_weight: '#10B981',
|
||||||
|
std_body_weight: '#10B981',
|
||||||
|
act_laying: '#1062B9',
|
||||||
|
std_laying: '#1062B9',
|
||||||
|
act_egg_weight: '#10B981',
|
||||||
|
std_egg_weight: '#10B981',
|
||||||
|
act_feed_intake: '#F52419',
|
||||||
|
std_feed_intake: '#F52419',
|
||||||
|
act_uniformity: '#F59E0B',
|
||||||
|
std_uniformity: '#F59E0B',
|
||||||
|
act_fcr: '#10B981',
|
||||||
|
std_fcr: '#10B981',
|
||||||
|
act_fcr_cum: '#F52419',
|
||||||
|
std_fcr_cum: '#10B981',
|
||||||
|
normal: '#10B981',
|
||||||
|
abnormal: '#F52419',
|
||||||
|
act_deplesi: '#10B981',
|
||||||
|
std_deplesi: '#10B981',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultLineColors: string[] = [
|
||||||
|
'#10B981',
|
||||||
|
'#1062B9',
|
||||||
|
'#F52419',
|
||||||
|
'#F59E0B',
|
||||||
|
'#7F56D9',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper function to get line color
|
||||||
|
const getLineColor = (seriesId: string | number, index: number): string => {
|
||||||
|
const predefinedColor = lineColors[seriesId];
|
||||||
|
if (predefinedColor) {
|
||||||
|
return predefinedColor;
|
||||||
|
}
|
||||||
|
return defaultLineColors[index % defaultLineColors.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapping for chart type labels
|
||||||
|
const chartTypeLabels: Record<keyof DashboardOverviewCharts, string> = {
|
||||||
|
body_weight: 'Body Weight',
|
||||||
|
performance: 'Performance',
|
||||||
|
fcr: 'FCR',
|
||||||
|
quality_control: 'Quality Control',
|
||||||
|
deplesi: 'Deplesi',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DashboardAllCharts = forwardRef<
|
||||||
|
DashboardAllChartsRef,
|
||||||
|
DashboardAllChartsProps
|
||||||
|
>(({ data, analysisMode }, ref) => {
|
||||||
|
// Create refs for charts - use string keys for flexibility
|
||||||
|
const chartRefs = useRef<{
|
||||||
|
[key: string]: HTMLDivElement | null;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
// Determine chart keys and labels based on analysis mode
|
||||||
|
const getChartConfig = () => {
|
||||||
|
if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) {
|
||||||
|
const overviewKeys: (keyof DashboardOverviewCharts)[] = [
|
||||||
|
'body_weight',
|
||||||
|
'performance',
|
||||||
|
'fcr',
|
||||||
|
'quality_control',
|
||||||
|
'deplesi',
|
||||||
|
];
|
||||||
|
return overviewKeys.map((key) => ({
|
||||||
|
key,
|
||||||
|
label: chartTypeLabels[key],
|
||||||
|
chartData: (data.charts as DashboardOverviewCharts)[key],
|
||||||
|
}));
|
||||||
|
} else if (
|
||||||
|
analysisMode === 'COMPARISON' &&
|
||||||
|
isComparisonCharts(data.charts)
|
||||||
|
) {
|
||||||
|
// For comparison mode, find which comparison type has data
|
||||||
|
const comparisonKey = data.charts.farm
|
||||||
|
? 'farm'
|
||||||
|
: data.charts.flock
|
||||||
|
? 'flock'
|
||||||
|
: 'kandang';
|
||||||
|
|
||||||
|
const comparisonLabels: Record<string, string> = {
|
||||||
|
farm: 'Farm Comparison',
|
||||||
|
flock: 'Flock Comparison',
|
||||||
|
kandang: 'Kandang Comparison',
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: comparisonKey,
|
||||||
|
label: comparisonLabels[comparisonKey],
|
||||||
|
chartData: data.charts[comparisonKey],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartConfig = getChartConfig();
|
||||||
|
|
||||||
|
// Expose method to get all chart refs
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getChartRefs: () => {
|
||||||
|
return chartConfig
|
||||||
|
.map(({ key, label }) => ({
|
||||||
|
key,
|
||||||
|
ref: chartRefs.current[key] || null,
|
||||||
|
label,
|
||||||
|
}))
|
||||||
|
.filter((item) => item.ref !== null);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-6'>
|
||||||
|
{chartConfig.map(({ key, label, chartData }) => {
|
||||||
|
if (
|
||||||
|
!chartData ||
|
||||||
|
!chartData.dataset ||
|
||||||
|
chartData.dataset.length === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesData: DashboardChartsSeries[] = chartData.series || [];
|
||||||
|
const dataset: DashboardChartsDataset[] = chartData.dataset || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
ref={(el: HTMLDivElement | null) => {
|
||||||
|
chartRefs.current[key] = el;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full rounded-lg',
|
||||||
|
}}
|
||||||
|
variant='bordered'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6'>
|
||||||
|
<div className='text-lg font-semibold'>
|
||||||
|
{label}{' '}
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:information-circle'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='inline text-neutral-500'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className='flex flex-wrap gap-3 mb-6'>
|
||||||
|
{seriesData.map((series, index) => {
|
||||||
|
const isStandard = series.id
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('std');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={series.id}
|
||||||
|
className='flex items-center gap-2 px-3 py-2 rounded-lg border border-neutral-400 bg-neutral-50'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-6 h-0.5 ${
|
||||||
|
isStandard ? 'border-t-2 border-dashed' : ''
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isStandard
|
||||||
|
? 'transparent'
|
||||||
|
: getLineColor(series.id, index),
|
||||||
|
borderColor: isStandard
|
||||||
|
? getLineColor(series.id, index)
|
||||||
|
: 'transparent',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<span className='text-sm text-neutral-900 font-medium'>
|
||||||
|
{series.label}
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:information-circle'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='text-neutral-400'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart */}
|
||||||
|
<ResponsiveContainer width='100%' height={350}>
|
||||||
|
<LineChart
|
||||||
|
data={dataset}
|
||||||
|
margin={{
|
||||||
|
top: 5,
|
||||||
|
right: 10,
|
||||||
|
left: 0,
|
||||||
|
bottom: 5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
||||||
|
<XAxis
|
||||||
|
dataKey='week'
|
||||||
|
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={{ stroke: '#e5e7eb' }}
|
||||||
|
label={{
|
||||||
|
value: 'Weeks',
|
||||||
|
position: 'insideBottom',
|
||||||
|
offset: -5,
|
||||||
|
style: { fontSize: 12, fill: '#9ca3af' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={{ stroke: '#e5e7eb' }}
|
||||||
|
domain={(() => {
|
||||||
|
const allValues: number[] = [];
|
||||||
|
dataset.forEach((item: DashboardChartsDataset) => {
|
||||||
|
seriesData.forEach((series) => {
|
||||||
|
const value = item[series.id];
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
allValues.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allValues.length === 0) return [0, 100];
|
||||||
|
|
||||||
|
const minValue = Math.min(...allValues);
|
||||||
|
const maxValue = Math.max(...allValues);
|
||||||
|
const padding = (maxValue - minValue) * 0.1;
|
||||||
|
const domainMin = Math.floor(
|
||||||
|
Math.max(0, minValue - padding)
|
||||||
|
);
|
||||||
|
const domainMax = Math.ceil(maxValue + padding);
|
||||||
|
|
||||||
|
return [domainMin, domainMax];
|
||||||
|
})()}
|
||||||
|
/>
|
||||||
|
{seriesData.map((series, index) => {
|
||||||
|
const isStandard = series.id
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('std');
|
||||||
|
const dataKey = series.id.toString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={series.id}
|
||||||
|
type='monotone'
|
||||||
|
dataKey={dataKey}
|
||||||
|
name={series.label}
|
||||||
|
stroke={getLineColor(series.id, index)}
|
||||||
|
opacity={isStandard ? 0.5 : 1}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray={isStandard ? '5 5' : undefined}
|
||||||
|
dot={
|
||||||
|
isStandard
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
r: 3,
|
||||||
|
fill: '#fff',
|
||||||
|
stroke: getLineColor(series.id, index),
|
||||||
|
strokeWidth: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeDot={isStandard ? undefined : { r: 5 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DashboardAllCharts.displayName = 'DashboardAllCharts';
|
||||||
|
|
||||||
|
export default DashboardAllCharts;
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
|
import { formatNumber } from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
Dashboard,
|
Dashboard,
|
||||||
DashboardOverviewCharts,
|
DashboardOverviewCharts,
|
||||||
@@ -25,20 +27,29 @@ import {
|
|||||||
type DashboardLineChartProps = {
|
type DashboardLineChartProps = {
|
||||||
analysisMode: 'OVERVIEW' | 'COMPARISON';
|
analysisMode: 'OVERVIEW' | 'COMPARISON';
|
||||||
data: Dashboard;
|
data: Dashboard;
|
||||||
|
selectedKandang?: OptionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Type guard to check if charts is DashboardOverviewCharts
|
// Type guard to check if charts is DashboardOverviewCharts
|
||||||
function isOverviewCharts(
|
function isOverviewCharts(
|
||||||
charts: DashboardOverviewCharts | DashboardComparisonCharts
|
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||||
): charts is DashboardOverviewCharts {
|
): charts is DashboardOverviewCharts {
|
||||||
return 'deplesi' in charts;
|
if (!charts) return false;
|
||||||
|
return (
|
||||||
|
'deplesi' in charts ||
|
||||||
|
'body_weight' in charts ||
|
||||||
|
'fcr' in charts ||
|
||||||
|
'performance' in charts ||
|
||||||
|
'quality_control' in charts
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type guard to check if charts is DashboardComparisonCharts
|
// Type guard to check if charts is DashboardComparisonCharts
|
||||||
function isComparisonCharts(
|
function isComparisonCharts(
|
||||||
charts: DashboardOverviewCharts | DashboardComparisonCharts
|
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||||
): charts is DashboardComparisonCharts {
|
): charts is DashboardComparisonCharts {
|
||||||
return 'location' in charts || 'flock' in charts || 'kandang' in charts;
|
if (!charts) return false;
|
||||||
|
return 'farm' in charts || 'flock' in charts || 'kandang' in charts;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineColors: Record<string, string> = {
|
const lineColors: Record<string, string> = {
|
||||||
@@ -94,6 +105,7 @@ const getLineColor = (
|
|||||||
const DashboardLineChart = ({
|
const DashboardLineChart = ({
|
||||||
analysisMode,
|
analysisMode,
|
||||||
data,
|
data,
|
||||||
|
selectedKandang,
|
||||||
}: DashboardLineChartProps) => {
|
}: DashboardLineChartProps) => {
|
||||||
const [chartData, setChartData] =
|
const [chartData, setChartData] =
|
||||||
useState<keyof DashboardOverviewCharts>('body_weight');
|
useState<keyof DashboardOverviewCharts>('body_weight');
|
||||||
@@ -123,7 +135,7 @@ const DashboardLineChart = ({
|
|||||||
isComparisonCharts(data.charts)
|
isComparisonCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
const comparisonChart =
|
const comparisonChart =
|
||||||
data.charts.location || data.charts.flock || data.charts.kandang;
|
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||||
seriesData = comparisonChart?.series || [];
|
seriesData = comparisonChart?.series || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +236,7 @@ const DashboardLineChart = ({
|
|||||||
isComparisonCharts(data.charts)
|
isComparisonCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
const comparisonChart =
|
const comparisonChart =
|
||||||
data.charts.location || data.charts.flock || data.charts.kandang;
|
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||||
seriesData = comparisonChart?.series || [];
|
seriesData = comparisonChart?.series || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +315,7 @@ const DashboardLineChart = ({
|
|||||||
// For COMPARISON mode, use the first available comparison chart
|
// For COMPARISON mode, use the first available comparison chart
|
||||||
if (isComparisonCharts(data.charts)) {
|
if (isComparisonCharts(data.charts)) {
|
||||||
const chartData =
|
const chartData =
|
||||||
data.charts.location ||
|
data.charts.farm ||
|
||||||
data.charts.flock ||
|
data.charts.flock ||
|
||||||
data.charts.kandang;
|
data.charts.kandang;
|
||||||
|
|
||||||
@@ -353,7 +365,7 @@ const DashboardLineChart = ({
|
|||||||
isComparisonCharts(data.charts)
|
isComparisonCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
const comparisonChart =
|
const comparisonChart =
|
||||||
data.charts.location ||
|
data.charts.farm ||
|
||||||
data.charts.flock ||
|
data.charts.flock ||
|
||||||
data.charts.kandang;
|
data.charts.kandang;
|
||||||
seriesData = comparisonChart?.series || [];
|
seriesData = comparisonChart?.series || [];
|
||||||
@@ -401,7 +413,7 @@ const DashboardLineChart = ({
|
|||||||
isComparisonCharts(data.charts)
|
isComparisonCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
const comparisonChart =
|
const comparisonChart =
|
||||||
data.charts.location ||
|
data.charts.farm ||
|
||||||
data.charts.flock ||
|
data.charts.flock ||
|
||||||
data.charts.kandang;
|
data.charts.kandang;
|
||||||
seriesData = comparisonChart?.series || [];
|
seriesData = comparisonChart?.series || [];
|
||||||
@@ -452,11 +464,84 @@ const DashboardLineChart = ({
|
|||||||
labelStyle={{ color: 'white', marginBottom: '4px' }}
|
labelStyle={{ color: 'white', marginBottom: '4px' }}
|
||||||
itemStyle={{ color: 'white', fontSize: '12px' }}
|
itemStyle={{ color: 'white', fontSize: '12px' }}
|
||||||
labelFormatter={(value) => `Week ${value}`}
|
labelFormatter={(value) => `Week ${value}`}
|
||||||
|
content={(props) => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-2 rounded-lg bg-neutral-950 p-4 text-white'>
|
||||||
|
<p className='text-neutral-300 text-xs font-semibold text-start'>
|
||||||
|
{analysisMode === 'OVERVIEW'
|
||||||
|
? selectedKandang
|
||||||
|
? selectedKandang.label || 'Overview Performance'
|
||||||
|
: 'Overview Performance'
|
||||||
|
: 'Comparison Performance'}
|
||||||
|
</p>
|
||||||
|
<ul className='flex flex-col gap-1'>
|
||||||
|
{props.payload.map((item, index) => {
|
||||||
|
if (item.name.startsWith('STD. ')) return null;
|
||||||
|
// Get series data to find the unit
|
||||||
|
let seriesData: DashboardChartsSeries[] = [];
|
||||||
|
if (
|
||||||
|
analysisMode === 'OVERVIEW' &&
|
||||||
|
isOverviewCharts(data.charts)
|
||||||
|
) {
|
||||||
|
seriesData = data.charts[chartData]?.series || [];
|
||||||
|
} else if (
|
||||||
|
analysisMode === 'COMPARISON' &&
|
||||||
|
isComparisonCharts(data.charts)
|
||||||
|
) {
|
||||||
|
const comparisonChart =
|
||||||
|
data.charts.farm ||
|
||||||
|
data.charts.flock ||
|
||||||
|
data.charts.kandang;
|
||||||
|
seriesData = comparisonChart?.series || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the series that matches this line's name
|
||||||
|
const series = seriesData.find(
|
||||||
|
(s) => s.label === item.name
|
||||||
|
);
|
||||||
|
const color = series?.id
|
||||||
|
? getLineColor(series.id, index, analysisMode)
|
||||||
|
: '#9ca3af';
|
||||||
|
const unit = series?.unit;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={item.name}
|
||||||
|
className='flex w-full justify-between items-center flex-row gap-6 p-0'
|
||||||
|
>
|
||||||
|
<span className='flex flex-row gap-1 items-center'>
|
||||||
|
<div
|
||||||
|
className='h-4 w-4 m-0 rounded-md'
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div className='m-0'>
|
||||||
|
{formatNumber(item.value)}
|
||||||
|
{unit}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span className='m-0'>{item.name}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<p className='text-neutral-300 text-xs text-start'>
|
||||||
|
Week {props.label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
formatter={(
|
formatter={(
|
||||||
value: number | undefined,
|
value: number | undefined,
|
||||||
name: string | undefined
|
name: string | undefined
|
||||||
) => {
|
) => {
|
||||||
if (value === undefined || name === undefined) return ['', ''];
|
if (
|
||||||
|
value === undefined ||
|
||||||
|
name === undefined ||
|
||||||
|
name.startsWith('STD. ')
|
||||||
|
)
|
||||||
|
return [undefined, undefined];
|
||||||
|
|
||||||
// Get series data to find the unit
|
// Get series data to find the unit
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
let seriesData: DashboardChartsSeries[] = [];
|
||||||
@@ -470,7 +555,7 @@ const DashboardLineChart = ({
|
|||||||
isComparisonCharts(data.charts)
|
isComparisonCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
const comparisonChart =
|
const comparisonChart =
|
||||||
data.charts.location ||
|
data.charts.farm ||
|
||||||
data.charts.flock ||
|
data.charts.flock ||
|
||||||
data.charts.kandang;
|
data.charts.kandang;
|
||||||
seriesData = comparisonChart?.series || [];
|
seriesData = comparisonChart?.series || [];
|
||||||
@@ -478,9 +563,9 @@ const DashboardLineChart = ({
|
|||||||
|
|
||||||
// Find the series that matches this line's name
|
// Find the series that matches this line's name
|
||||||
const series = seriesData.find((s) => s.label === name);
|
const series = seriesData.find((s) => s.label === name);
|
||||||
const unit = series?.unit || '';
|
const id = series?.id || '';
|
||||||
|
|
||||||
return [`${value} ${unit}`, name];
|
return [value, id];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* Dynamic Line rendering based on visible series */}
|
{/* Dynamic Line rendering based on visible series */}
|
||||||
@@ -497,9 +582,7 @@ const DashboardLineChart = ({
|
|||||||
isComparisonCharts(data.charts)
|
isComparisonCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
const comparisonChart =
|
const comparisonChart =
|
||||||
data.charts.location ||
|
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||||
data.charts.flock ||
|
|
||||||
data.charts.kandang;
|
|
||||||
seriesData = comparisonChart?.series || [];
|
seriesData = comparisonChart?.series || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +640,7 @@ const DashboardLineChart = ({
|
|||||||
isComparisonCharts(data.charts)
|
isComparisonCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
const comparisonChart =
|
const comparisonChart =
|
||||||
data.charts.location || data.charts.flock || data.charts.kandang;
|
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||||
dataset = comparisonChart?.dataset || [];
|
dataset = comparisonChart?.dataset || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import jsPDF from 'jspdf';
|
||||||
|
import { toPng } from 'html-to-image';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { formatDate } from '@/lib/helper';
|
||||||
|
import { DashboardFilterType } from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
||||||
|
import { DashboardAllChartsRef } from '@/components/pages/dashboard/chart/DashboardAllCharts';
|
||||||
|
|
||||||
|
interface DashboardPDFExportParams {
|
||||||
|
filterValues: DashboardFilterType;
|
||||||
|
statsRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
allChartsRef: React.RefObject<DashboardAllChartsRef | null>;
|
||||||
|
setExporting: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateDashboardPDF = async ({
|
||||||
|
filterValues,
|
||||||
|
statsRef,
|
||||||
|
allChartsRef,
|
||||||
|
setExporting,
|
||||||
|
}: DashboardPDFExportParams): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setExporting(true);
|
||||||
|
toast.loading('Generating PDF...', { id: 'export-pdf' });
|
||||||
|
|
||||||
|
// Wait for DOM to update
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||||
|
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||||
|
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||||
|
const margin = 10;
|
||||||
|
let yPosition = margin;
|
||||||
|
|
||||||
|
// Add title
|
||||||
|
pdf.setFontSize(16);
|
||||||
|
pdf.setFont('helvetica', 'bold');
|
||||||
|
pdf.text('Dashboard Produksi', margin, yPosition);
|
||||||
|
yPosition += 10;
|
||||||
|
|
||||||
|
// Add filter information (horizontal layout)
|
||||||
|
pdf.setFontSize(6);
|
||||||
|
pdf.setFont('helvetica', 'normal');
|
||||||
|
|
||||||
|
const filterItems: string[] = [];
|
||||||
|
|
||||||
|
// Period
|
||||||
|
if (filterValues.startDate || filterValues.endDate) {
|
||||||
|
const periodText = `Periode: ${
|
||||||
|
filterValues.startDate
|
||||||
|
? formatDate(filterValues.startDate, 'DD MMM YYYY')
|
||||||
|
: '-'
|
||||||
|
} s.d ${
|
||||||
|
filterValues.endDate
|
||||||
|
? formatDate(filterValues.endDate, 'DD MMM YYYY')
|
||||||
|
: '-'
|
||||||
|
}`;
|
||||||
|
filterItems.push(periodText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analysis Mode
|
||||||
|
const analysisModeText = `Analysis Mode: ${
|
||||||
|
filterValues.analysisMode === 'OVERVIEW'
|
||||||
|
? 'Performance Overview'
|
||||||
|
: 'Performance Comparison'
|
||||||
|
}`;
|
||||||
|
filterItems.push(analysisModeText);
|
||||||
|
|
||||||
|
// Comparison Type (only for COMPARISON mode)
|
||||||
|
if (
|
||||||
|
filterValues.analysisMode === 'COMPARISON' &&
|
||||||
|
filterValues.comparisonType
|
||||||
|
) {
|
||||||
|
const comparisonTypeLabel =
|
||||||
|
filterValues.comparisonType === 'FARM'
|
||||||
|
? 'Farm'
|
||||||
|
: filterValues.comparisonType === 'FLOCK'
|
||||||
|
? 'Flock'
|
||||||
|
: filterValues.comparisonType === 'KANDANG'
|
||||||
|
? 'Kandang'
|
||||||
|
: filterValues.comparisonType;
|
||||||
|
filterItems.push(`Compared By: ${comparisonTypeLabel}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Farm
|
||||||
|
if (filterValues.location) {
|
||||||
|
const locationText = Array.isArray(filterValues.location)
|
||||||
|
? filterValues.location.map((loc) => loc.label).join(', ')
|
||||||
|
: filterValues.location.label;
|
||||||
|
filterItems.push(`Farm: ${locationText || '-'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flock
|
||||||
|
if (
|
||||||
|
filterValues.flock &&
|
||||||
|
(Array.isArray(filterValues.flock)
|
||||||
|
? filterValues.flock.length > 0
|
||||||
|
: filterValues.flock)
|
||||||
|
) {
|
||||||
|
const flockText = Array.isArray(filterValues.flock)
|
||||||
|
? filterValues.flock.map((f) => f.label).join(', ')
|
||||||
|
: filterValues.flock.label;
|
||||||
|
filterItems.push(`Flock: ${flockText || '-'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kandang
|
||||||
|
if (
|
||||||
|
filterValues.kandang &&
|
||||||
|
(Array.isArray(filterValues.kandang)
|
||||||
|
? filterValues.kandang.length > 0
|
||||||
|
: filterValues.kandang)
|
||||||
|
) {
|
||||||
|
const kandangText = Array.isArray(filterValues.kandang)
|
||||||
|
? filterValues.kandang.map((k) => k.label).join(', ')
|
||||||
|
: filterValues.kandang.label;
|
||||||
|
filterItems.push(`Kandang: ${kandangText || '-'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated timestamp
|
||||||
|
filterItems.push(`Dicetak: ${formatDate(new Date(), 'DD MMM YYYY HH:mm')}`);
|
||||||
|
|
||||||
|
// Render filter items horizontally with word wrap and gray background
|
||||||
|
const maxWidth = pageWidth - 2 * margin;
|
||||||
|
let currentLine = '';
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
// First pass: calculate all lines
|
||||||
|
filterItems.forEach((item, index) => {
|
||||||
|
const separator = index > 0 ? ' | ' : '';
|
||||||
|
const testLine = currentLine + separator + item;
|
||||||
|
const testWidth = pdf.getTextWidth(testLine);
|
||||||
|
|
||||||
|
if (testWidth > maxWidth && currentLine !== '') {
|
||||||
|
lines.push(currentLine);
|
||||||
|
currentLine = item;
|
||||||
|
} else {
|
||||||
|
currentLine = testLine;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add last line
|
||||||
|
if (currentLine) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate background dimensions
|
||||||
|
const lineHeight = 3;
|
||||||
|
const padding = 1;
|
||||||
|
const backgroundHeight = lines.length * lineHeight + padding * 2;
|
||||||
|
|
||||||
|
// Draw gray background
|
||||||
|
pdf.setFillColor(240, 240, 240); // Light gray (RGB: 240, 240, 240)
|
||||||
|
pdf.rect(
|
||||||
|
margin - padding,
|
||||||
|
yPosition - padding - 2,
|
||||||
|
pageWidth - 2 * margin + padding * 2,
|
||||||
|
backgroundHeight,
|
||||||
|
'F'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render text on top of background
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
pdf.text(line, margin, yPosition);
|
||||||
|
if (index < lines.length - 1) {
|
||||||
|
yPosition += lineHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
yPosition += 10;
|
||||||
|
|
||||||
|
// Capture and add stats if available
|
||||||
|
if (statsRef.current) {
|
||||||
|
const statsImage = await toPng(statsRef.current, {
|
||||||
|
quality: 1,
|
||||||
|
pixelRatio: 2,
|
||||||
|
});
|
||||||
|
const statsImgProps = pdf.getImageProperties(statsImage);
|
||||||
|
const statsWidth = pageWidth - 2 * margin;
|
||||||
|
const statsHeight =
|
||||||
|
(statsImgProps.height * statsWidth) / statsImgProps.width;
|
||||||
|
|
||||||
|
// Check if we need a new page
|
||||||
|
if (yPosition + statsHeight > pageHeight - margin) {
|
||||||
|
pdf.addPage();
|
||||||
|
yPosition = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf.addImage(
|
||||||
|
statsImage,
|
||||||
|
'PNG',
|
||||||
|
margin,
|
||||||
|
yPosition,
|
||||||
|
statsWidth,
|
||||||
|
statsHeight
|
||||||
|
);
|
||||||
|
yPosition += statsHeight + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChartsRef.current) {
|
||||||
|
// Get all individual chart refs
|
||||||
|
const chartRefs = allChartsRef.current.getChartRefs();
|
||||||
|
|
||||||
|
// Capture each chart separately and add to PDF
|
||||||
|
for (let i = 0; i < chartRefs.length; i++) {
|
||||||
|
const { ref: chartElement, label } = chartRefs[i];
|
||||||
|
|
||||||
|
if (chartElement) {
|
||||||
|
// Add chart title
|
||||||
|
pdf.setFontSize(12);
|
||||||
|
pdf.setFont('helvetica', 'bold');
|
||||||
|
|
||||||
|
const chartImage = await toPng(chartElement, {
|
||||||
|
quality: 1,
|
||||||
|
pixelRatio: 2,
|
||||||
|
});
|
||||||
|
const chartImgProps = pdf.getImageProperties(chartImage);
|
||||||
|
const chartWidth = pageWidth - 2 * margin;
|
||||||
|
const chartHeight =
|
||||||
|
(chartImgProps.height * chartWidth) / chartImgProps.width;
|
||||||
|
|
||||||
|
// Calculate total height needed (title + spacing + chart)
|
||||||
|
const titleHeight = 10;
|
||||||
|
const totalHeight = titleHeight + chartHeight;
|
||||||
|
|
||||||
|
// Check if chart fits on current page
|
||||||
|
if (yPosition + totalHeight > pageHeight - margin) {
|
||||||
|
pdf.addPage();
|
||||||
|
yPosition = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add title
|
||||||
|
pdf.text(label, margin, yPosition);
|
||||||
|
yPosition += titleHeight;
|
||||||
|
|
||||||
|
// Add chart image
|
||||||
|
pdf.addImage(
|
||||||
|
chartImage,
|
||||||
|
'PNG',
|
||||||
|
margin,
|
||||||
|
yPosition,
|
||||||
|
chartWidth,
|
||||||
|
chartHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update yPosition for next chart (add spacing between charts)
|
||||||
|
yPosition += chartHeight + 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the PDF
|
||||||
|
const fileName = `dashboard-production-${new Date().toISOString().split('T')[0]}.pdf`;
|
||||||
|
pdf.save(fileName);
|
||||||
|
|
||||||
|
toast.success('PDF exported successfully!', { id: 'export-pdf' });
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to export PDF. Please try again.', {
|
||||||
|
id: 'export-pdf',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ export type DashboardFilterType = {
|
|||||||
analysisMode: string;
|
analysisMode: string;
|
||||||
comparisonType: string | undefined;
|
comparisonType: string | undefined;
|
||||||
location: OptionType | OptionType[];
|
location: OptionType | OptionType[];
|
||||||
lokasiIds: number[] | undefined;
|
locationIds: number[] | undefined;
|
||||||
flock: OptionType | OptionType[] | undefined;
|
flock: OptionType | OptionType[] | undefined;
|
||||||
flockIds: number[] | undefined;
|
flockIds: number[] | undefined;
|
||||||
kandang: OptionType | OptionType[] | undefined;
|
kandang: OptionType | OptionType[] | undefined;
|
||||||
@@ -25,7 +25,7 @@ export const DashboardFilterOverviewSchema: yup.ObjectSchema<DashboardFilterType
|
|||||||
then: (schema) => schema.required('Compared by is required'),
|
then: (schema) => schema.required('Compared by is required'),
|
||||||
otherwise: (schema) => schema.optional(),
|
otherwise: (schema) => schema.optional(),
|
||||||
}),
|
}),
|
||||||
lokasiIds: yup.array().optional(),
|
locationIds: yup.array().optional(),
|
||||||
flockIds: yup.array().optional(),
|
flockIds: yup.array().optional(),
|
||||||
kandangIds: yup.array().optional(),
|
kandangIds: yup.array().optional(),
|
||||||
location: yup
|
location: yup
|
||||||
@@ -68,7 +68,7 @@ export const DashboardFilterComparisonSchema: yup.ObjectSchema<DashboardFilterTy
|
|||||||
then: (schema) => schema.required('Compared by is required'),
|
then: (schema) => schema.required('Compared by is required'),
|
||||||
otherwise: (schema) => schema.optional(),
|
otherwise: (schema) => schema.optional(),
|
||||||
}),
|
}),
|
||||||
lokasiIds: yup.array().optional(),
|
locationIds: yup.array().optional(),
|
||||||
flockIds: yup.array().optional(),
|
flockIds: yup.array().optional(),
|
||||||
kandangIds: yup.array().optional(),
|
kandangIds: yup.array().optional(),
|
||||||
location: yup
|
location: yup
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-7xl pb-16'>
|
<section className='w-full max-w-full pb-16'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href='/expense'
|
||||||
@@ -65,7 +65,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
tabs={expenseDetailTabs}
|
tabs={expenseDetailTabs}
|
||||||
variant='lifted'
|
variant='lifted'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'max-w-5xl mx-auto mt-4',
|
wrapper: 'mx-auto mt-4',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const ExpenseRealizationContent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
<RequirePermission permissions='lti.expense.update.realization'>
|
<RequirePermission permissions='lti.expense.update.realization'>
|
||||||
<Button
|
<Button
|
||||||
@@ -84,7 +84,7 @@ const ExpenseRealizationContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='overflow-x-auto w-full max-w-5xl mx-auto'>
|
<div className='overflow-x-auto w-full mx-auto'>
|
||||||
<table className='table table-sm table-zebra'>
|
<table className='table table-sm table-zebra'>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -179,7 +179,7 @@ const ExpenseRealizationContent = ({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div className='w-full mt-8 mx-auto'>
|
||||||
<div className='flex flex-row gap-4'>
|
<div className='flex flex-row gap-4'>
|
||||||
<Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}>
|
<Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}>
|
||||||
<div className='w-full flex flex-col gap-2'>
|
<div className='w-full flex flex-col gap-2'>
|
||||||
@@ -216,127 +216,141 @@ const ExpenseRealizationContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div className='w-full mt-8 mx-auto grid grid-cols-2 gap-4'>
|
||||||
<h2 className='font-bold text-xl text-center'>
|
<div>
|
||||||
Rincian Pengajuan Biaya Operasional
|
<h2 className='font-bold text-xl text-center'>
|
||||||
</h2>
|
Rincian Pengajuan Biaya Operasional
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
{initialValues?.kandangs.map(
|
||||||
let expenseGrandTotal = 0;
|
(kandangExpense, kandangExpenseIdx) => {
|
||||||
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
kandangExpense.pengajuans?.forEach(
|
kandangExpense.pengajuans?.forEach(
|
||||||
(item) => (expenseGrandTotal += item.qty * item.price)
|
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={kandangExpenseIdx}
|
key={kandangExpenseIdx}
|
||||||
className='overflow-x-auto w-full mx-auto'
|
className='overflow-x-auto w-full mx-auto'
|
||||||
>
|
>
|
||||||
<table className='table table-sm table-zebra'>
|
<table className='table table-sm table-zebra'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
colSpan={5}
|
colSpan={5}
|
||||||
className='font-bold text-center text-base-content text-lg'
|
className='font-bold text-center text-base-content text-lg'
|
||||||
>
|
>
|
||||||
Biaya {kandangExpense.name}
|
Biaya {kandangExpense.name}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nonstock</th>
|
|
||||||
<th>Total Kuantitas</th>
|
|
||||||
<th>Total Biaya</th>
|
|
||||||
<th>Catatan</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{kandangExpense.pengajuans?.map(
|
|
||||||
(pengajuanItem, pengajuanIdx) => (
|
|
||||||
<tr key={pengajuanIdx}>
|
|
||||||
<td>{pengajuanItem.nonstock.name}</td>
|
|
||||||
<td>{pengajuanItem.qty}</td>
|
|
||||||
<td>{formatCurrency(pengajuanItem.price)}</td>
|
|
||||||
<td className='w-xs'>{pengajuanItem.note ?? '-'}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)
|
<tr>
|
||||||
)}
|
<th>Nonstock</th>
|
||||||
</tbody>
|
<th>Total Kuantitas</th>
|
||||||
<tfoot>
|
<th>Total Biaya</th>
|
||||||
<tr className='border-y'>
|
<th>Catatan</th>
|
||||||
<th colSpan={2} className='text-right'>
|
</tr>
|
||||||
Total Biaya Keseluruhan:
|
</thead>
|
||||||
</th>
|
<tbody>
|
||||||
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th>
|
{kandangExpense.pengajuans?.map(
|
||||||
</tr>
|
(pengajuanItem, pengajuanIdx) => (
|
||||||
</tfoot>
|
<tr key={pengajuanIdx}>
|
||||||
</table>
|
<td>{pengajuanItem.nonstock.name}</td>
|
||||||
</div>
|
<td>{pengajuanItem.qty}</td>
|
||||||
);
|
<td>{formatCurrency(pengajuanItem.price)}</td>
|
||||||
})}
|
<td className='w-xs'>
|
||||||
|
{pengajuanItem.notes ?? '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className='border-y'>
|
||||||
|
<th colSpan={2} className='text-right'>
|
||||||
|
Total Biaya Keseluruhan:
|
||||||
|
</th>
|
||||||
|
<th colSpan={2}>
|
||||||
|
{formatCurrency(expenseGrandTotal)}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div>
|
||||||
<h2 className='font-bold text-xl text-center'>
|
<h2 className='font-bold text-xl text-center'>
|
||||||
Rincian Realisasi Biaya Operasional
|
Rincian Realisasi Biaya Operasional
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
{initialValues?.kandangs.map(
|
||||||
let expenseGrandTotal = 0;
|
(kandangExpense, kandangExpenseIdx) => {
|
||||||
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
kandangExpense.realisasi?.forEach(
|
kandangExpense.realisasi?.forEach(
|
||||||
(item) => (expenseGrandTotal += item.qty * item.price)
|
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={kandangExpenseIdx}
|
key={kandangExpenseIdx}
|
||||||
className='overflow-x-auto w-full mx-auto'
|
className='overflow-x-auto w-full mx-auto'
|
||||||
>
|
>
|
||||||
<table className='table table-sm table-zebra'>
|
<table className='table table-sm table-zebra'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
colSpan={5}
|
colSpan={5}
|
||||||
className='font-bold text-center text-base-content text-lg'
|
className='font-bold text-center text-base-content text-lg'
|
||||||
>
|
>
|
||||||
Biaya {kandangExpense.name}
|
Biaya {kandangExpense.name}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nonstock</th>
|
|
||||||
<th>Total Kuantitas</th>
|
|
||||||
<th>Total Biaya</th>
|
|
||||||
<th>Catatan</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{kandangExpense.realisasi?.map(
|
|
||||||
(realisasiItem, realisasiIdx) => (
|
|
||||||
<tr key={realisasiIdx}>
|
|
||||||
<td>{realisasiItem.nonstock.name}</td>
|
|
||||||
<td>{realisasiItem.qty}</td>
|
|
||||||
<td>{formatCurrency(realisasiItem.price)}</td>
|
|
||||||
<td className='w-xs'>{realisasiItem.note ?? '-'}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)
|
<tr>
|
||||||
)}
|
<th>Nonstock</th>
|
||||||
</tbody>
|
<th>Total Kuantitas</th>
|
||||||
<tfoot>
|
<th>Total Biaya</th>
|
||||||
<tr className='border-y'>
|
<th>Catatan</th>
|
||||||
<th colSpan={2} className='text-right'>
|
</tr>
|
||||||
Total Biaya Keseluruhan:
|
</thead>
|
||||||
</th>
|
<tbody>
|
||||||
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th>
|
{kandangExpense.realisasi?.map(
|
||||||
</tr>
|
(realisasiItem, realisasiIdx) => (
|
||||||
</tfoot>
|
<tr key={realisasiIdx}>
|
||||||
</table>
|
<td>{realisasiItem.nonstock.name}</td>
|
||||||
</div>
|
<td>{realisasiItem.qty}</td>
|
||||||
);
|
<td>{formatCurrency(realisasiItem.price)}</td>
|
||||||
})}
|
<td className='w-xs'>
|
||||||
|
{realisasiItem.notes ?? '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className='border-y'>
|
||||||
|
<th colSpan={2} className='text-right'>
|
||||||
|
Total Biaya Keseluruhan:
|
||||||
|
</th>
|
||||||
|
<th colSpan={2}>
|
||||||
|
{formatCurrency(expenseGrandTotal)}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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,69 +174,84 @@ const ExpenseKandangsTable = ({
|
|||||||
updateSortingFilter('picSort', picSortFilter);
|
updateSortingFilter('picSort', picSortFilter);
|
||||||
}, [sorting, updateSortingFilter]);
|
}, [sorting, updateSortingFilter]);
|
||||||
|
|
||||||
return (
|
// Tampilkan tabel jika:
|
||||||
<Card
|
// 1. Mode request pertama kali (type='add' dan formType='request')
|
||||||
className={{
|
// 2. Atau sudah ada kandang yang dipilih
|
||||||
wrapper: className?.wrapper,
|
const shouldShowTable =
|
||||||
body: 'p-4 shadow',
|
(type === 'add' && formType === 'request') ||
|
||||||
}}
|
(selectedKandangs.length > 0 && selectedKandangs.some((k) => k.id));
|
||||||
>
|
|
||||||
<Collapse
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
title={
|
|
||||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
|
||||||
<div className='card-title'>Pilih Kandang</div>
|
|
||||||
|
|
||||||
<Icon
|
return (
|
||||||
icon='material-symbols:keyboard-arrow-down'
|
<>
|
||||||
width={24}
|
{shouldShowTable && (
|
||||||
height={24}
|
<Card
|
||||||
className={cn('text-primary transition-transform', {
|
|
||||||
'-rotate-180': open,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
className='w-full!'
|
|
||||||
titleClassName='w-full p-0!'
|
|
||||||
>
|
|
||||||
<Table<Kandang>
|
|
||||||
data={isResponseSuccess(kandangs) ? kandangs?.data : []}
|
|
||||||
columns={kandangsColumns}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
|
|
||||||
}
|
|
||||||
onPageChange={setPage}
|
|
||||||
isLoading={isLoading}
|
|
||||||
sorting={sorting}
|
|
||||||
setSorting={setSorting}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
setRowSelection={setRowSelection}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
wrapper: className?.wrapper,
|
||||||
'mb-20':
|
body: 'p-4 shadow',
|
||||||
isResponseSuccess(kandangs) && kandangs?.data?.length === 0,
|
|
||||||
}),
|
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 first:flex first:flex-row first:justify-start',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 first:flex first:flex-row first:justify-start',
|
|
||||||
paginationClassName: cn({
|
|
||||||
hidden:
|
|
||||||
isResponseSuccess(kandangs) &&
|
|
||||||
kandangs?.meta?.total_pages === 1,
|
|
||||||
}),
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Collapse>
|
<Collapse
|
||||||
</Card>
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
|
<div className='card-title'>
|
||||||
|
{formType === 'realization'
|
||||||
|
? 'Kandang yang Direalisasikan'
|
||||||
|
: 'Pilih Kandang'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
|
>
|
||||||
|
<Table<Kandang>
|
||||||
|
data={isResponseSuccess(kandangs) ? kandangs?.data : []}
|
||||||
|
columns={kandangsColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'mb-20':
|
||||||
|
isResponseSuccess(kandangs) && kandangs?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-6 py-3 text-xs font-semibold text-gray-500 first:flex first:flex-row first:justify-start',
|
||||||
|
bodyRowClassName: 'border-b border-b-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-6 py-3 first:flex first:flex-row first:justify-start',
|
||||||
|
paginationClassName: cn({
|
||||||
|
hidden:
|
||||||
|
isResponseSuccess(kandangs) &&
|
||||||
|
kandangs?.meta?.total_pages === 1,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export const getExpenseRealizationFormInitialValues = (
|
|||||||
? formatDate(initialValues?.realization_date, 'YYYY-MM-DD')
|
? formatDate(initialValues?.realization_date, 'YYYY-MM-DD')
|
||||||
: undefined,
|
: undefined,
|
||||||
kandangs: initialValues?.kandangs.map((kandang) => ({
|
kandangs: initialValues?.kandangs.map((kandang) => ({
|
||||||
id: kandang.kandang_id,
|
id: kandang.id,
|
||||||
name: kandang.name,
|
name: kandang.name,
|
||||||
})),
|
})),
|
||||||
supplier: initialValues?.supplier
|
supplier: initialValues?.supplier
|
||||||
@@ -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(
|
||||||
formik.setFieldTouched('location', true);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('location', val);
|
const location = val as OptionType | null;
|
||||||
|
const locationId = location ? Number(location.value) : 0;
|
||||||
|
|
||||||
const locationId = Array.isArray(val) ? val[0]?.value : val?.value;
|
formik.setFieldTouched('location', true);
|
||||||
formik.setFieldValue('location_id', locationId);
|
formik.setFieldValue('location', location);
|
||||||
|
formik.setFieldTouched('location_id', true);
|
||||||
formik.setFieldValue('kandangs', []);
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
},
|
||||||
// Auto-create expense item for location (without kandang)
|
[]
|
||||||
formik.setFieldValue('expense_nonstocks', [
|
);
|
||||||
{
|
|
||||||
cost_items: [
|
|
||||||
{
|
|
||||||
nonstock: null,
|
|
||||||
nonstock_id: 0,
|
|
||||||
quantity: undefined,
|
|
||||||
price: undefined,
|
|
||||||
notes: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const kandangsChangeHandler = (
|
const kandangsChangeHandler = (
|
||||||
kandangs: { id?: number; name?: string }[]
|
kandangs: { id?: number; name?: string }[]
|
||||||
@@ -268,6 +256,7 @@ const ExpenseRequestForm = ({
|
|||||||
|
|
||||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched('supplier', true);
|
formik.setFieldTouched('supplier', true);
|
||||||
|
formik.setFieldTouched('supplier_id', true);
|
||||||
formik.setFieldValue('supplier', val);
|
formik.setFieldValue('supplier', val);
|
||||||
|
|
||||||
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
|
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
|
||||||
@@ -360,7 +349,7 @@ const ExpenseRequestForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-5xl'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href='/expense'
|
||||||
@@ -407,6 +396,16 @@ const ExpenseRequestForm = ({
|
|||||||
placeholder='Pilih Kategori'
|
placeholder='Pilih Kategori'
|
||||||
value={formik.values.category}
|
value={formik.values.category}
|
||||||
onChange={categoryChangeHandler}
|
onChange={categoryChangeHandler}
|
||||||
|
isError={
|
||||||
|
formik.touched.category && Boolean(formik.errors.category)
|
||||||
|
}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.category && formik.errors.category
|
||||||
|
? typeof formik.errors.category === 'object'
|
||||||
|
? 'Kategori wajib diisi!'
|
||||||
|
: (formik.errors.category as string)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 'BOP',
|
value: 'BOP',
|
||||||
@@ -427,8 +426,13 @@ const ExpenseRequestForm = ({
|
|||||||
value={formik.values.location}
|
value={formik.values.location}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isLoading={isLoadingLocationOptions}
|
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
isError={
|
||||||
|
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.location_id as string}
|
||||||
|
isClearable
|
||||||
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -438,6 +442,12 @@ const ExpenseRequestForm = ({
|
|||||||
required
|
required
|
||||||
value={formik.values.transaction_date}
|
value={formik.values.transaction_date}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.transaction_date &&
|
||||||
|
Boolean(formik.errors.transaction_date)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.transaction_date as string}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
}}
|
}}
|
||||||
@@ -460,8 +470,12 @@ const ExpenseRequestForm = ({
|
|||||||
value={formik.values.supplier}
|
value={formik.values.supplier}
|
||||||
onChange={supplierChangeHandler}
|
onChange={supplierChangeHandler}
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
isLoading={isLoadingVendorOptions}
|
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
|
isLoading={isLoadingVendorOptions}
|
||||||
|
isError={
|
||||||
|
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.supplier_id as string}
|
||||||
className={{ wrapper: 'col-span-12' }}
|
className={{ wrapper: 'col-span-12' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`,
|
||||||
|
true
|
||||||
|
);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||||
val
|
val
|
||||||
@@ -96,7 +100,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isExpenseRepeaterInputError = (
|
const isExpenseRepeaterInputError = (
|
||||||
column: 'nonstock' | 'quantity' | 'price' | 'notes',
|
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
||||||
kandangExpenseIdx: number,
|
kandangExpenseIdx: number,
|
||||||
expenseIdx: number
|
expenseIdx: number
|
||||||
) => {
|
) => {
|
||||||
@@ -105,11 +109,14 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
expenseIdx
|
expenseIdx
|
||||||
]?.[column] &&
|
]?.[column] &&
|
||||||
Boolean(
|
Boolean(
|
||||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx] &&
|
||||||
Object &&
|
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx] ===
|
||||||
|
'object' &&
|
||||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||||
expenseIdx
|
expenseIdx
|
||||||
] instanceof Object &&
|
] &&
|
||||||
|
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx]
|
||||||
|
.cost_items?.[expenseIdx] === 'object' &&
|
||||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||||
expenseIdx
|
expenseIdx
|
||||||
]?.[column]
|
]?.[column]
|
||||||
@@ -117,6 +124,32 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getExpenseRepeaterErrorMessage = (
|
||||||
|
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
||||||
|
kandangExpenseIdx: number,
|
||||||
|
expenseIdx: number
|
||||||
|
): string => {
|
||||||
|
const kandangError = formik.errors.expense_nonstocks?.[kandangExpenseIdx];
|
||||||
|
|
||||||
|
if (!kandangError || typeof kandangError !== 'object') return '';
|
||||||
|
|
||||||
|
if (!('cost_items' in kandangError)) return '';
|
||||||
|
|
||||||
|
const costItemsError = kandangError.cost_items?.[expenseIdx];
|
||||||
|
|
||||||
|
if (!costItemsError || typeof costItemsError !== 'object') return '';
|
||||||
|
|
||||||
|
const fieldError = costItemsError[column as keyof typeof costItemsError];
|
||||||
|
|
||||||
|
if (!fieldError) return '';
|
||||||
|
|
||||||
|
if (typeof fieldError === 'object' && fieldError !== null) {
|
||||||
|
return 'Nonstock wajib diisi!';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(fieldError);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
@@ -202,10 +235,21 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
val
|
val
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
isError={isExpenseRepeaterInputError(
|
||||||
|
'nonstock_id',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'nonstock_id',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
options={nonstockOptions}
|
options={nonstockOptions}
|
||||||
isLoading={isLoadingNonstockOptions}
|
isLoading={isLoadingNonstockOptions}
|
||||||
onInputChange={setNonstockInputValue}
|
onInputChange={setNonstockInputValue}
|
||||||
className={{ wrapper: 'min-w-48' }}
|
className={{ wrapper: 'min-w-48' }}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -226,6 +270,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
kandangExpenseIdx,
|
kandangExpenseIdx,
|
||||||
expenseIdx
|
expenseIdx
|
||||||
)}
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'quantity',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
className={{ wrapper: 'min-w-24' }}
|
className={{ wrapper: 'min-w-24' }}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@@ -246,6 +295,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
kandangExpenseIdx,
|
kandangExpenseIdx,
|
||||||
expenseIdx
|
expenseIdx
|
||||||
)}
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'price',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
inputPrefix={
|
inputPrefix={
|
||||||
<span className='text-gray-600 font-medium'>
|
<span className='text-gray-600 font-medium'>
|
||||||
Rp
|
Rp
|
||||||
@@ -271,6 +325,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
kandangExpenseIdx,
|
kandangExpenseIdx,
|
||||||
expenseIdx
|
expenseIdx
|
||||||
)}
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'notes',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
className={{ wrapper: 'min-w-24' }}
|
className={{ wrapper: 'min-w-24' }}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Pihak',
|
label: 'Pihak',
|
||||||
value: finance.party.id ? finance.party.name : '-',
|
value: finance.party?.id ? finance.party?.name : '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Tanggal',
|
label: 'Tanggal',
|
||||||
@@ -56,25 +56,21 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Nomor Rekening',
|
label: 'Nomor Rekening',
|
||||||
value: `${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
|
value: `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `Rekening ${formatTitleCase(finance.party.type)}`,
|
label: `Rekening ${formatTitleCase(finance.party?.type)}`,
|
||||||
value: finance.party.account_number,
|
value: finance.party?.account_number,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Nominal',
|
label: 'Nominal',
|
||||||
value: formatCurrency(finance.expense_amount),
|
value: formatCurrency(finance.nominal),
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Sisa',
|
|
||||||
value: formatCurrency(finance.income_amount),
|
|
||||||
},
|
},
|
||||||
].filter((item) => {
|
].filter((item) => {
|
||||||
// Hide party account number row if transaction type is INJECTION
|
// Hide party account number row if transaction type is INJECTION
|
||||||
if (
|
if (
|
||||||
FINANCE_INJECTION_STATUS.includes(finance.transaction_type) &&
|
FINANCE_INJECTION_STATUS.includes(finance.transaction_type) &&
|
||||||
item.label === `Rekening ${formatTitleCase(finance.party.type)}`
|
item.label === `Rekening ${formatTitleCase(finance.party?.type)}`
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -148,18 +144,19 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className='flex flex-row gap-2 justify-end'>
|
<div className='flex flex-row gap-2 justify-end'>
|
||||||
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && (
|
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) &&
|
||||||
<RequirePermission permissions='lti.finance.payments.update'>
|
finance.party?.type !== 'SUPPLIER' && (
|
||||||
<Button
|
<RequirePermission permissions='lti.finance.payments.update'>
|
||||||
color='warning'
|
<Button
|
||||||
className='min-w-24'
|
color='warning'
|
||||||
href={`/finance/detail/edit?financeId=${finance.id}`}
|
className='min-w-24'
|
||||||
>
|
href={`/finance/detail/edit?financeId=${finance.id}`}
|
||||||
<Icon icon='mdi:pencil-outline' />
|
>
|
||||||
Edit
|
<Icon icon='mdi:pencil-outline' />
|
||||||
</Button>
|
Edit
|
||||||
</RequirePermission>
|
</Button>
|
||||||
)}
|
</RequirePermission>
|
||||||
|
)}
|
||||||
{FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && (
|
{FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && (
|
||||||
<RequirePermission permissions='lti.finance.initial_balances.update'>
|
<RequirePermission permissions='lti.finance.initial_balances.update'>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} 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';
|
||||||
import { BankApi } from '@/services/api/master-data';
|
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
|
||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
@@ -65,19 +65,24 @@ const RowOptionsMenu = ({
|
|||||||
|
|
||||||
{FINANCE_TRANSACTION_STATUS.includes(
|
{FINANCE_TRANSACTION_STATUS.includes(
|
||||||
props.row.original.transaction_type
|
props.row.original.transaction_type
|
||||||
) && (
|
) &&
|
||||||
<RequirePermission permissions='lti.finance.payments.update'>
|
props.row.original.party?.type !== 'SUPPLIER' && (
|
||||||
<Button
|
<RequirePermission permissions='lti.finance.payments.update'>
|
||||||
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
|
<Button
|
||||||
variant='ghost'
|
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
|
||||||
color='warning'
|
variant='ghost'
|
||||||
className='justify-start text-sm'
|
color='warning'
|
||||||
>
|
className='justify-start text-sm'
|
||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
>
|
||||||
Edit
|
<Icon
|
||||||
</Button>
|
icon='material-symbols:edit-outline'
|
||||||
</RequirePermission>
|
width={16}
|
||||||
)}
|
height={16}
|
||||||
|
/>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
{FINANCE_INITIAL_BALANCE_STATUS.includes(
|
{FINANCE_INITIAL_BALANCE_STATUS.includes(
|
||||||
props.row.original.transaction_type
|
props.row.original.transaction_type
|
||||||
@@ -194,20 +199,25 @@ const FinanceTable = () => {
|
|||||||
|
|
||||||
// ===== Options =====
|
// ===== Options =====
|
||||||
const transactionTypeOptions = useMemo(() => {
|
const transactionTypeOptions = useMemo(() => {
|
||||||
return [
|
|
||||||
{ label: 'Transfer', value: 'TRANSFER' },
|
|
||||||
{ label: 'Cash', value: 'CASH' },
|
|
||||||
{ label: 'Card', value: 'CARD' },
|
|
||||||
{ label: 'Cheque', value: 'CHEQUE' },
|
|
||||||
{ label: 'Saldo', value: 'SALDO' },
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
const partyTypeOptions = useMemo(() => {
|
|
||||||
return [
|
return [
|
||||||
{ label: 'Customer', value: 'CUSTOMER' },
|
{ label: 'Customer', value: 'CUSTOMER' },
|
||||||
{ label: 'Supplier', value: 'SUPPLIER' },
|
{ label: 'Supplier', value: 'SUPPLIER' },
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
const {
|
||||||
|
options: partyTypeOptions,
|
||||||
|
isLoadingOptions: partyTypeIsLoadingOptions,
|
||||||
|
setInputValue: partyTypeInputValue,
|
||||||
|
loadMore: partyTypeLoadMore,
|
||||||
|
} = useSelect(
|
||||||
|
selectedTransactionType
|
||||||
|
? selectedTransactionType.value === 'CUSTOMER'
|
||||||
|
? CustomerApi.basePath
|
||||||
|
: SupplierApi.basePath
|
||||||
|
: '',
|
||||||
|
'id',
|
||||||
|
'name'
|
||||||
|
);
|
||||||
const sortByOptions = useMemo(() => {
|
const sortByOptions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
|
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
|
||||||
@@ -336,10 +346,10 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pihak',
|
header: 'Pihak',
|
||||||
accessorFn: (finance: Finance) => finance.party.name,
|
accessorFn: (finance: Finance) => finance.party?.name,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
if (props.row.original.party.id) {
|
if (props.row.original.party?.id) {
|
||||||
return <span>{props.row.original.party.name}</span>;
|
return <span>{props.row.original.party?.name}</span>;
|
||||||
}
|
}
|
||||||
return <span>{'-'}</span>;
|
return <span>{'-'}</span>;
|
||||||
},
|
},
|
||||||
@@ -360,12 +370,12 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Bank',
|
header: 'Bank',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorFn: (finance: Finance) =>
|
||||||
`${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
|
`${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pengeluaran (Rp)',
|
header: 'Pengeluaran (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorFn: (finance: Finance) =>
|
||||||
formatCurrency(finance.expense_amount),
|
formatCurrency(Math.abs(finance.expense_amount)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pemasukan (Rp)',
|
header: 'Pemasukan (Rp)',
|
||||||
@@ -468,25 +478,41 @@ const FinanceTable = () => {
|
|||||||
<div className='grid grid-cols-4 gap-6'>
|
<div className='grid grid-cols-4 gap-6'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={transactionTypeOptions}
|
options={transactionTypeOptions}
|
||||||
label='Jenis Transaksi'
|
label='Tipe Transaksi'
|
||||||
value={selectedTransactionType}
|
value={selectedTransactionType}
|
||||||
onChange={transactionTypeChangeHandler}
|
onChange={transactionTypeChangeHandler}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
<SelectInput
|
||||||
|
options={partyTypeOptions}
|
||||||
|
label={
|
||||||
|
selectedTransactionType
|
||||||
|
? selectedTransactionType.value === 'CUSTOMER'
|
||||||
|
? 'Pelanggan'
|
||||||
|
: 'Supplier'
|
||||||
|
: 'Pihak'
|
||||||
|
}
|
||||||
|
value={selectedPartyType}
|
||||||
|
onChange={partyTypeChangeHandler}
|
||||||
|
onInputChange={partyTypeInputValue}
|
||||||
|
onMenuScrollToBottom={partyTypeLoadMore}
|
||||||
|
isLoading={partyTypeIsLoadingOptions}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={
|
options={
|
||||||
isResponseSuccess(bankRawData)
|
isResponseSuccess(bankRawData)
|
||||||
? bankOptions.map((bank) => ({
|
? bankOptions.map((bank) => ({
|
||||||
label:
|
label:
|
||||||
bankRawData.data.find((data) => data.id === bank.value)
|
bankRawData.data.find((data) => data.id === bank?.value)
|
||||||
?.alias +
|
?.alias +
|
||||||
' - ' +
|
' - ' +
|
||||||
bankRawData.data.find((data) => data.id === bank.value)
|
bankRawData.data.find((data) => data.id === bank?.value)
|
||||||
?.account_number +
|
?.account_number +
|
||||||
' - ' +
|
' - ' +
|
||||||
bankRawData.data.find((data) => data.id === bank.value)
|
bankRawData.data.find((data) => data.id === bank?.value)
|
||||||
?.owner,
|
?.owner,
|
||||||
value: bank.value,
|
value: bank?.value,
|
||||||
}))
|
}))
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
@@ -497,13 +523,6 @@ const FinanceTable = () => {
|
|||||||
onMenuScrollToBottom={bankLoadMore}
|
onMenuScrollToBottom={bankLoadMore}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
|
||||||
options={partyTypeOptions}
|
|
||||||
label='Pihak'
|
|
||||||
value={selectedPartyType}
|
|
||||||
onChange={partyTypeChangeHandler}
|
|
||||||
isClearable
|
|
||||||
/>
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
label='Cari'
|
label='Cari'
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ import {
|
|||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import Alert from '@/components/Alert';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
interface FormFinanceAddProps {
|
interface FormFinanceAddProps {
|
||||||
type?: 'add' | 'edit';
|
type?: 'add' | 'edit';
|
||||||
@@ -51,18 +53,22 @@ const FormFinanceAdd = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: FormFinanceAddProps) => {
|
}: FormFinanceAddProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [serverErrorMessage, setServerErrorMessage] = useState('');
|
||||||
|
const [isSupplier, setIsSupplier] = useState(
|
||||||
|
initialValues?.party?.type === 'SUPPLIER'
|
||||||
|
);
|
||||||
|
|
||||||
// ===== Formik =====
|
// ===== Formik =====
|
||||||
const formikInitialValues = useMemo((): FinanceFormValues => {
|
const formikInitialValues = useMemo((): FinanceFormValues => {
|
||||||
return {
|
return {
|
||||||
party_type_option:
|
party_type_option:
|
||||||
FINANCE_PARTY_TYPE_OPTIONS.find(
|
FINANCE_PARTY_TYPE_OPTIONS.find(
|
||||||
(option) => option.value === initialValues?.party.type
|
(option) => option.value === initialValues?.party?.type
|
||||||
) || null,
|
) || null,
|
||||||
party_id_option: initialValues?.party
|
party_id_option: initialValues?.party
|
||||||
? {
|
? {
|
||||||
label: initialValues?.party.name || '',
|
label: initialValues?.party?.name || '',
|
||||||
value: initialValues?.party.id || 0,
|
value: initialValues?.party?.id || 0,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
payment_date: initialValues?.payment_date || '',
|
payment_date: initialValues?.payment_date || '',
|
||||||
@@ -72,11 +78,11 @@ const FormFinanceAdd = ({
|
|||||||
) || null,
|
) || null,
|
||||||
bank_id_option: initialValues?.bank
|
bank_id_option: initialValues?.bank
|
||||||
? {
|
? {
|
||||||
label: initialValues.bank.name,
|
label: initialValues?.bank?.name,
|
||||||
value: initialValues.bank.id,
|
value: initialValues?.bank?.id,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
party_account_number: initialValues?.party.account_number || '',
|
party_account_number: initialValues?.party?.account_number || '',
|
||||||
reference_number: initialValues?.reference_number || '',
|
reference_number: initialValues?.reference_number || '',
|
||||||
nominal: initialValues?.nominal.toString() || '',
|
nominal: initialValues?.nominal.toString() || '',
|
||||||
notes: initialValues?.notes || '',
|
notes: initialValues?.notes || '',
|
||||||
@@ -153,6 +159,7 @@ const FormFinanceAdd = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +175,7 @@ const FormFinanceAdd = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +215,7 @@ const FormFinanceAdd = ({
|
|||||||
? formik.errors.party_type_option
|
? formik.errors.party_type_option
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
isDisabled={type === 'edit' || isSupplier}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
@@ -245,7 +254,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!formik.values.party_type_option?.value}
|
isDisabled={!formik.values.party_type_option?.value || isSupplier}
|
||||||
/>
|
/>
|
||||||
<DateInput
|
<DateInput
|
||||||
label='Tanggal'
|
label='Tanggal'
|
||||||
@@ -263,6 +272,7 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Metode Pembayaran'
|
label='Metode Pembayaran'
|
||||||
@@ -284,6 +294,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
|
isDisabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Bank'
|
label='Bank'
|
||||||
@@ -324,6 +335,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
|
isDisabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={`Nomor Rekening ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'Pihak'}`}
|
label={`Nomor Rekening ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'Pihak'}`}
|
||||||
@@ -344,6 +356,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
readOnly
|
readOnly
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label='Nomor Referensi'
|
label='Nomor Referensi'
|
||||||
@@ -363,6 +376,7 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label='Nominal'
|
label='Nominal'
|
||||||
@@ -378,6 +392,7 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
label='Catatan'
|
label='Catatan'
|
||||||
@@ -393,8 +408,18 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
{serverErrorMessage && (
|
||||||
|
<Alert color='error'>
|
||||||
|
<Icon icon='mdi:alert' />
|
||||||
|
{serverErrorMessage}
|
||||||
|
<Button color='error' onClick={() => setServerErrorMessage('')}>
|
||||||
|
<Icon icon='mdi:close' />
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<div className='flex justify-center gap-4'>
|
<div className='flex justify-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='reset'
|
||||||
|
|||||||
+1
-7
@@ -27,13 +27,7 @@ export const InitialBalanceFormSchema = Yup.object().shape({
|
|||||||
'Pihak wajib diisi',
|
'Pihak wajib diisi',
|
||||||
(value) => value !== null && value !== undefined
|
(value) => value !== null && value !== undefined
|
||||||
),
|
),
|
||||||
bank_id_option: Yup.mixed()
|
bank_id_option: Yup.mixed().nullable(),
|
||||||
.nullable()
|
|
||||||
.test(
|
|
||||||
'is-valid-option',
|
|
||||||
'Bank wajib diisi',
|
|
||||||
(value) => value !== null && value !== undefined
|
|
||||||
),
|
|
||||||
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
|
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
|
||||||
initial_balance_type_option: Yup.mixed()
|
initial_balance_type_option: Yup.mixed()
|
||||||
.nullable()
|
.nullable()
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ import { Bank } from '@/types/api/master-data/bank';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import Alert from '@/components/Alert';
|
||||||
|
|
||||||
interface FormFinanceAddInitialBalanceProps {
|
interface FormFinanceAddInitialBalanceProps {
|
||||||
type?: 'add' | 'edit';
|
type?: 'add' | 'edit';
|
||||||
@@ -42,6 +43,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: FormFinanceAddInitialBalanceProps) => {
|
}: FormFinanceAddInitialBalanceProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [serverErrorMessage, setServerErrorMessage] = useState('');
|
||||||
|
|
||||||
// ===== Formik =====
|
// ===== Formik =====
|
||||||
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
|
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
|
||||||
@@ -53,18 +55,18 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
return {
|
return {
|
||||||
party_type_option:
|
party_type_option:
|
||||||
FINANCE_PARTY_TYPE_OPTIONS.find(
|
FINANCE_PARTY_TYPE_OPTIONS.find(
|
||||||
(option) => option.value === initialValues?.party.type
|
(option) => option.value === initialValues?.party?.type
|
||||||
) || null,
|
) || null,
|
||||||
party_id_option: initialValues?.party
|
party_id_option: initialValues?.party
|
||||||
? {
|
? {
|
||||||
label: initialValues.party.name,
|
label: initialValues.party?.name,
|
||||||
value: initialValues.party.id,
|
value: initialValues.party?.id,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
bank_id_option: initialValues?.bank
|
bank_id_option: initialValues?.bank
|
||||||
? {
|
? {
|
||||||
label: initialValues.bank.name,
|
label: initialValues.bank?.name,
|
||||||
value: initialValues.bank.id,
|
value: initialValues.bank?.id,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
reference_number: initialValues?.reference_number || '',
|
reference_number: initialValues?.reference_number || '',
|
||||||
@@ -147,6 +149,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +169,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +215,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
isDisabled={type === 'edit'}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -277,7 +282,6 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
? formik.errors.bank_id_option
|
? formik.errors.bank_id_option
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -362,7 +366,18 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
{serverErrorMessage && (
|
||||||
|
<Alert color='error'>
|
||||||
|
<Icon icon='mdi:alert' />
|
||||||
|
{serverErrorMessage}
|
||||||
|
<Button color='error' onClick={() => setServerErrorMessage('')}>
|
||||||
|
<Icon icon='mdi:close' />
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='flex justify-center gap-4'>
|
<div className='flex justify-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='reset'
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ import {
|
|||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import Alert from '@/components/Alert';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
interface FormFinanceInjectionProps {
|
interface FormFinanceInjectionProps {
|
||||||
type?: 'add' | 'edit';
|
type?: 'add' | 'edit';
|
||||||
@@ -37,14 +39,15 @@ const FormFinanceInjection = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: FormFinanceInjectionProps) => {
|
}: FormFinanceInjectionProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [serverErrorMessage, setServerErrorMessage] = useState('');
|
||||||
|
|
||||||
// ===== Formik =====
|
// ===== Formik =====
|
||||||
const formikInitialValues = useMemo((): InjectionFormValues => {
|
const formikInitialValues = useMemo((): InjectionFormValues => {
|
||||||
return {
|
return {
|
||||||
bank_id_option: initialValues?.bank
|
bank_id_option: initialValues?.bank
|
||||||
? {
|
? {
|
||||||
label: initialValues.bank.name,
|
label: initialValues.bank?.name,
|
||||||
value: initialValues.bank.id,
|
value: initialValues.bank?.id,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
adjustment_date: initialValues?.payment_date || '',
|
adjustment_date: initialValues?.payment_date || '',
|
||||||
@@ -103,6 +106,7 @@ const FormFinanceInjection = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +123,7 @@ const FormFinanceInjection = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +235,15 @@ const FormFinanceInjection = ({
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
{serverErrorMessage && (
|
||||||
|
<Alert color='error'>
|
||||||
|
<Icon icon='mdi:alert' />
|
||||||
|
{serverErrorMessage}
|
||||||
|
<Button color='error' onClick={() => setServerErrorMessage('')}>
|
||||||
|
<Icon icon='mdi:close' />
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<div className='flex justify-center gap-4'>
|
<div className='flex justify-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='reset'
|
||||||
|
|||||||
@@ -110,6 +110,14 @@ const DeliveryProductObjectSchema = Yup.object({
|
|||||||
.typeError('Qty harus berupa angka!'),
|
.typeError('Qty harus berupa angka!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DeliveryDocumentSchema = Yup.mixed<File | MovementDocument>()
|
||||||
|
.nullable()
|
||||||
|
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value): boolean => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
||||||
delivery_cost: Yup.number()
|
delivery_cost: Yup.number()
|
||||||
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
||||||
@@ -135,13 +143,7 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
|||||||
}),
|
}),
|
||||||
document_path: Yup.string().nullable().optional(),
|
document_path: Yup.string().nullable().optional(),
|
||||||
document_index: Yup.number().optional(),
|
document_index: Yup.number().optional(),
|
||||||
document: Yup.mixed<File | MovementDocument>()
|
document: DeliveryDocumentSchema,
|
||||||
.nullable()
|
|
||||||
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
|
|
||||||
if (!value) return true;
|
|
||||||
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
driver_name: Yup.string().required('Nama sopir wajib diisi!'),
|
driver_name: Yup.string().required('Nama sopir wajib diisi!'),
|
||||||
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
|
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
|
||||||
supplier: Yup.object({
|
supplier: Yup.object({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
@@ -95,7 +95,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
isLoadingOptions: isLoadingWarehouses,
|
isLoadingOptions: isLoadingWarehouses,
|
||||||
loadMore: loadMoreWarehouses,
|
loadMore: loadMoreWarehouses,
|
||||||
rawData: warehouses,
|
rawData: warehouses,
|
||||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search');
|
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search', {
|
||||||
|
flag: 'EKSPEDISI',
|
||||||
|
});
|
||||||
|
|
||||||
// ===== SELECT INPUT DATA =====
|
// ===== SELECT INPUT DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -261,6 +263,47 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const prevSourceWarehouseIdRef = useRef<number | null>(
|
||||||
|
formik.values.source_warehouse_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===== RESET PRODUCTS WHEN SOURCE WAREHOUSE CHANGES =====
|
||||||
|
useEffect(() => {
|
||||||
|
const prevSourceWarehouseId = prevSourceWarehouseIdRef.current;
|
||||||
|
const currentSourceWarehouseId = formik.values.source_warehouse_id;
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
||||||
|
prevSourceWarehouseId !== null
|
||||||
|
) {
|
||||||
|
formik.setFieldValue('products', [
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
formik.setFieldTouched('products', false);
|
||||||
|
|
||||||
|
const updatedDeliveries = formik.values.deliveries.map(
|
||||||
|
(delivery: DeliverySchema) => ({
|
||||||
|
...delivery,
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
|
formik.setFieldTouched('deliveries', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
||||||
|
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
|
||||||
|
|
||||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||||
const {
|
const {
|
||||||
setInputValue: setProductWarehouseSelectInputValue,
|
setInputValue: setProductWarehouseSelectInputValue,
|
||||||
@@ -347,13 +390,71 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTransferDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
formik.setFieldValue('transfer_date', e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== EVENT HANDLERS =====
|
// ===== EVENT HANDLERS =====
|
||||||
// Product Handlers
|
const handleTransferDateChange = useCallback(
|
||||||
const addProduct = () => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
formik.setFieldValue('transfer_date', e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSourceWarehouseChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newSourceWarehouseId = (val as WarehouseOptionType)?.value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newSourceWarehouseId &&
|
||||||
|
newSourceWarehouseId === formik.values.destination_warehouse_id
|
||||||
|
) {
|
||||||
|
const destinationWarehouseName =
|
||||||
|
(formik.values.destination_warehouse as WarehouseOptionType)?.label ||
|
||||||
|
'gudang tujuan';
|
||||||
|
toast.error(
|
||||||
|
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.setFieldTouched('source_warehouse', true);
|
||||||
|
formik.setFieldValue('source_warehouse', val);
|
||||||
|
formik.setFieldTouched('source_warehouse_id', true);
|
||||||
|
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
formik.values.destination_warehouse_id,
|
||||||
|
formik.values.destination_warehouse,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDestinationWarehouseChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newDestinationWarehouseId = (val as WarehouseOptionType)?.value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newDestinationWarehouseId &&
|
||||||
|
newDestinationWarehouseId === formik.values.source_warehouse_id
|
||||||
|
) {
|
||||||
|
const sourceWarehouseName =
|
||||||
|
(formik.values.source_warehouse as WarehouseOptionType)?.label ||
|
||||||
|
'gudang asal';
|
||||||
|
toast.error(
|
||||||
|
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.setFieldTouched('destination_warehouse', true);
|
||||||
|
formik.setFieldValue('destination_warehouse', val);
|
||||||
|
formik.setFieldTouched('destination_warehouse_id', true);
|
||||||
|
formik.setFieldValue(
|
||||||
|
'destination_warehouse_id',
|
||||||
|
newDestinationWarehouseId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[formik.values.source_warehouse_id, formik.values.source_warehouse]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addProduct = useCallback(() => {
|
||||||
const newProducts = [
|
const newProducts = [
|
||||||
...(formik.values.products || []),
|
...(formik.values.products || []),
|
||||||
{
|
{
|
||||||
@@ -363,22 +464,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
formik.setFieldValue('products', newProducts);
|
formik.setFieldValue('products', newProducts);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const removeProduct = useCallback(
|
const removeProduct = useCallback((i: number) => {
|
||||||
(i: number) => {
|
const updatedProducts =
|
||||||
const updatedProducts =
|
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
||||||
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
if (index !== i) {
|
||||||
if (index !== i) {
|
acc.push(item);
|
||||||
acc.push(item);
|
}
|
||||||
}
|
return acc;
|
||||||
return acc;
|
}, []) ?? [];
|
||||||
}, []) ?? [];
|
|
||||||
|
|
||||||
formik.setFieldValue('products', updatedProducts);
|
formik.setFieldValue('products', updatedProducts);
|
||||||
},
|
}, []);
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const bulkRemoveProduct = useCallback(() => {
|
const bulkRemoveProduct = useCallback(() => {
|
||||||
const updatedProducts =
|
const updatedProducts =
|
||||||
@@ -387,10 +485,45 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
) ?? [];
|
) ?? [];
|
||||||
formik.setFieldValue('products', updatedProducts);
|
formik.setFieldValue('products', updatedProducts);
|
||||||
setSelectedProducts([]);
|
setSelectedProducts([]);
|
||||||
}, [formik, selectedProducts]);
|
}, [formik, selectedProducts, setSelectedProducts]);
|
||||||
|
|
||||||
// Delivery Handlers
|
const handleProductChange = useCallback(
|
||||||
const addDelivery = () => {
|
(idx: number, val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched(`products.${idx}.product`, true);
|
||||||
|
formik.setFieldValue(`products.${idx}.product`, val);
|
||||||
|
formik.setFieldTouched(`products.${idx}.product_id`, true);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`products.${idx}.product_id`,
|
||||||
|
(val as ProductWarehouseOptionType)?.value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProductSelectAllChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedProducts(formik.values.products?.map((_, idx) => idx) ?? []);
|
||||||
|
} else {
|
||||||
|
setSelectedProducts([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[formik.values.products, setSelectedProducts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProductCheckboxChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const idx = Number(e.target.name.replace('product-', ''));
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedProducts((prev) => [...prev, idx]);
|
||||||
|
} else {
|
||||||
|
setSelectedProducts((prev) => prev.filter((i) => i !== idx));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSelectedProducts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addDelivery = useCallback(() => {
|
||||||
formik.setFieldValue('deliveries', [
|
formik.setFieldValue('deliveries', [
|
||||||
...(formik.values.deliveries || []),
|
...(formik.values.deliveries || []),
|
||||||
{
|
{
|
||||||
@@ -410,25 +543,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const removeDelivery = useCallback(
|
const removeDelivery = useCallback((i: number) => {
|
||||||
(i: number) => {
|
const updatedDeliveries =
|
||||||
const updatedDeliveries =
|
formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => {
|
||||||
formik.values.deliveries?.reduce(
|
if (index !== i) {
|
||||||
(acc: DeliverySchema[], item, index) => {
|
acc.push(item);
|
||||||
if (index !== i) {
|
}
|
||||||
acc.push(item);
|
return acc;
|
||||||
}
|
}, []) ?? [];
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
},
|
}, []);
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const bulkRemoveDelivery = useCallback(() => {
|
const bulkRemoveDelivery = useCallback(() => {
|
||||||
const updatedDeliveries =
|
const updatedDeliveries =
|
||||||
@@ -437,33 +564,101 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
) ?? [];
|
) ?? [];
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
setSelectedDeliveries([]);
|
setSelectedDeliveries([]);
|
||||||
}, [formik, selectedDeliveries]);
|
}, [formik, selectedDeliveries, setSelectedDeliveries]);
|
||||||
|
|
||||||
// Cost Calculation Handlers
|
const handleDeliverySelectAllChange = useCallback(
|
||||||
const handleDeliveryCostChange = useCallback(
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(idx: number, value: number) => {
|
if (e.target.checked) {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
setSelectedDeliveries(
|
||||||
|
formik.values.deliveries?.map((_, idx) => idx) ?? []
|
||||||
const delivery = formik.values.deliveries?.[idx];
|
|
||||||
if (delivery) {
|
|
||||||
const productQty = delivery.products.reduce(
|
|
||||||
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
|
||||||
0
|
|
||||||
);
|
);
|
||||||
if (productQty > 0 && value > 0) {
|
} else {
|
||||||
const perItem = value / productQty;
|
setSelectedDeliveries([]);
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.delivery_cost_per_item`,
|
|
||||||
perItem
|
|
||||||
);
|
|
||||||
} else if (value === 0) {
|
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik]
|
[formik.values.deliveries, setSelectedDeliveries]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDeliveryCheckboxChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const idx = Number(e.target.name.replace('delivery-', ''));
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedDeliveries((prev) => [...prev, idx]);
|
||||||
|
} else {
|
||||||
|
setSelectedDeliveries((prev) => prev.filter((i) => i !== idx));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSelectedDeliveries]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeliveryProductChange = useCallback(
|
||||||
|
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`deliveries.${deliveryIdx}.products.0.product`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||||
|
(val as OptionType)?.value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeliverySupplierChange = useCallback(
|
||||||
|
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
|
||||||
|
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
|
||||||
|
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`deliveries.${deliveryIdx}.supplier_id`,
|
||||||
|
(val as OptionType)?.value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeliveryDocumentChange = useCallback(
|
||||||
|
(deliveryIdx: number, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
toast.error('Ukuran dokumen maksimal 5 MB!');
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeliveryCostChange = useCallback((idx: number, value: number) => {
|
||||||
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
||||||
|
|
||||||
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
|
if (delivery) {
|
||||||
|
const productQty = delivery.products.reduce(
|
||||||
|
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
if (productQty > 0 && value > 0) {
|
||||||
|
const perItem = value / productQty;
|
||||||
|
formik.setFieldValue(
|
||||||
|
`deliveries.${idx}.delivery_cost_per_item`,
|
||||||
|
perItem
|
||||||
|
);
|
||||||
|
} else if (value === 0) {
|
||||||
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDeliveryCostPerItemChange = useCallback(
|
const handleDeliveryCostPerItemChange = useCallback(
|
||||||
(idx: number, value: number) => {
|
(idx: number, value: number) => {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
||||||
@@ -482,7 +677,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostChangeWrapper = useCallback(
|
const handleDeliveryCostChangeWrapper = useCallback(
|
||||||
@@ -957,43 +1152,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih gudang asal...'
|
placeholder='Pilih gudang asal...'
|
||||||
value={formik.values.source_warehouse}
|
value={formik.values.source_warehouse}
|
||||||
onChange={(val) => {
|
onChange={handleSourceWarehouseChange}
|
||||||
const newSourceWarehouseId = (val as WarehouseOptionType)
|
|
||||||
?.value;
|
|
||||||
|
|
||||||
if (newSourceWarehouseId) {
|
|
||||||
if (
|
|
||||||
newSourceWarehouseId ===
|
|
||||||
formik.values.destination_warehouse_id
|
|
||||||
) {
|
|
||||||
const destinationWarehouseName =
|
|
||||||
(
|
|
||||||
formik.values
|
|
||||||
.destination_warehouse as WarehouseOptionType
|
|
||||||
)?.label || 'gudang tujuan';
|
|
||||||
|
|
||||||
toast.error(
|
|
||||||
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.setFieldTouched('source_warehouse', true);
|
|
||||||
formik.setFieldValue('source_warehouse', val);
|
|
||||||
formik.setFieldTouched('source_warehouse_id', true);
|
|
||||||
formik.setFieldValue(
|
|
||||||
'source_warehouse_id',
|
|
||||||
newSourceWarehouseId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
formik.errors.destination_warehouse_id ===
|
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
|
||||||
) {
|
|
||||||
formik.setFieldError('destination_warehouse_id', undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
onInputChange={setWarehouseSelectInputValue}
|
onInputChange={setWarehouseSelectInputValue}
|
||||||
onMenuScrollToBottom={loadMoreWarehouses}
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
@@ -1057,41 +1216,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih gudang tujuan...'
|
placeholder='Pilih gudang tujuan...'
|
||||||
value={formik.values.destination_warehouse}
|
value={formik.values.destination_warehouse}
|
||||||
onChange={(val) => {
|
onChange={handleDestinationWarehouseChange}
|
||||||
const newDestinationWarehouseId = (val as WarehouseOptionType)
|
|
||||||
?.value;
|
|
||||||
|
|
||||||
if (newDestinationWarehouseId) {
|
|
||||||
if (
|
|
||||||
newDestinationWarehouseId ===
|
|
||||||
formik.values.source_warehouse_id
|
|
||||||
) {
|
|
||||||
const sourceWarehouseName =
|
|
||||||
(formik.values.source_warehouse as WarehouseOptionType)
|
|
||||||
?.label || 'gudang asal';
|
|
||||||
|
|
||||||
toast.error(
|
|
||||||
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.setFieldTouched('destination_warehouse', true);
|
|
||||||
formik.setFieldValue('destination_warehouse', val);
|
|
||||||
formik.setFieldTouched('destination_warehouse_id', true);
|
|
||||||
formik.setFieldValue(
|
|
||||||
'destination_warehouse_id',
|
|
||||||
newDestinationWarehouseId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
formik.errors.destination_warehouse_id ===
|
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
|
||||||
) {
|
|
||||||
formik.setFieldError('destination_warehouse_id', undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
onInputChange={setWarehouseSelectInputValue}
|
onInputChange={setWarehouseSelectInputValue}
|
||||||
isLoading={isLoadingWarehouses}
|
isLoading={isLoadingWarehouses}
|
||||||
@@ -1165,18 +1290,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
selectedProducts.length &&
|
selectedProducts.length &&
|
||||||
formik.values.products?.length > 0
|
formik.values.products?.length > 0
|
||||||
}
|
}
|
||||||
onChange={(
|
onChange={handleProductSelectAllChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedProducts(
|
|
||||||
formik.values.products?.map((_, idx) => idx) ??
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedProducts([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1213,17 +1327,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name={`product-${idx}`}
|
name={`product-${idx}`}
|
||||||
checked={selectedProducts.includes(idx)}
|
checked={selectedProducts.includes(idx)}
|
||||||
onChange={(
|
onChange={handleProductCheckboxChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedProducts([...selectedProducts, idx]);
|
|
||||||
} else {
|
|
||||||
setSelectedProducts(
|
|
||||||
selectedProducts.filter((i) => i !== idx)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1235,24 +1339,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
value={product.product ?? undefined}
|
value={product.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) => handleProductChange(idx, val)}
|
||||||
formik.setFieldTouched(
|
|
||||||
`products.${idx}.product`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`products.${idx}.product`,
|
|
||||||
val
|
|
||||||
);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`products.${idx}.product_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`products.${idx}.product_id`,
|
|
||||||
(val as ProductWarehouseOptionType)?.value
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={productWarehouseOptions}
|
options={productWarehouseOptions}
|
||||||
onInputChange={setProductWarehouseSelectInputValue}
|
onInputChange={setProductWarehouseSelectInputValue}
|
||||||
onMenuScrollToBottom={loadMoreProductWarehouses}
|
onMenuScrollToBottom={loadMoreProductWarehouses}
|
||||||
@@ -1379,19 +1466,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
selectedDeliveries.length &&
|
selectedDeliveries.length &&
|
||||||
formik.values.deliveries?.length > 0
|
formik.values.deliveries?.length > 0
|
||||||
}
|
}
|
||||||
onChange={(
|
onChange={handleDeliverySelectAllChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedDeliveries(
|
|
||||||
formik.values.deliveries?.map(
|
|
||||||
(_, idx) => idx
|
|
||||||
) ?? []
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedDeliveries([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1474,20 +1549,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name={`delivery-${idx}`}
|
name={`delivery-${idx}`}
|
||||||
checked={selectedDeliveries.includes(idx)}
|
checked={selectedDeliveries.includes(idx)}
|
||||||
onChange={(
|
onChange={handleDeliveryCheckboxChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedDeliveries([
|
|
||||||
...selectedDeliveries,
|
|
||||||
idx,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setSelectedDeliveries(
|
|
||||||
selectedDeliveries.filter((i) => i !== idx)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1500,24 +1562,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
placeholder='Pilih produk...'
|
placeholder='Pilih produk...'
|
||||||
value={delivery.products[0]?.product ?? undefined}
|
value={delivery.products[0]?.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
formik.setFieldTouched(
|
handleDeliveryProductChange(idx, val)
|
||||||
`deliveries.${idx}.products.0.product`,
|
}
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.products.0.product`,
|
|
||||||
val
|
|
||||||
);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`deliveries.${idx}.products.0.product_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.products.0.product_id`,
|
|
||||||
(val as OptionType)?.value
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={getFilteredProductWarehouseOptions()}
|
options={getFilteredProductWarehouseOptions()}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
@@ -1568,24 +1615,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
placeholder='Pilih supplier...'
|
placeholder='Pilih supplier...'
|
||||||
value={delivery.supplier}
|
value={delivery.supplier}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
formik.setFieldTouched(
|
handleDeliverySupplierChange(idx, val)
|
||||||
`deliveries.${idx}.supplier`,
|
}
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.supplier`,
|
|
||||||
val
|
|
||||||
);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`deliveries.${idx}.supplier_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.supplier_id`,
|
|
||||||
(val as OptionType)?.value
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
onInputChange={setSupplierSelectInputValue}
|
onInputChange={setSupplierSelectInputValue}
|
||||||
isLoading={isLoadingSuppliers}
|
isLoading={isLoadingSuppliers}
|
||||||
@@ -1677,20 +1709,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<FileInput
|
<FileInput
|
||||||
accept='.pdf,.jpg,.jpeg,.png'
|
accept='.pdf,.jpg,.jpeg,.png'
|
||||||
name={`deliveries.${idx}.document`}
|
name={`deliveries.${idx}.document`}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
const file = e.target.files?.[0];
|
handleDeliveryDocumentChange(idx, e)
|
||||||
if (file) {
|
}
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
toast.error('Ukuran dokumen maksimal 5 MB!');
|
|
||||||
e.target.value = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.document`,
|
|
||||||
file
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...isRepeaterInputError(
|
{...isRepeaterInputError(
|
||||||
'deliveries',
|
'deliveries',
|
||||||
'document',
|
'document',
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const InventoryProductDetail = ({
|
|||||||
<td>:</td>
|
<td>:</td>
|
||||||
<td>
|
<td>
|
||||||
{inventoryProduct?.tax
|
{inventoryProduct?.tax
|
||||||
? formatCurrency(inventoryProduct?.tax)
|
? formatNumber(inventoryProduct?.tax) + '%'
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
|
|||||||
import { TableToolbar } from '@/components/table/TableToolbar';
|
import { TableToolbar } from '@/components/table/TableToolbar';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
MarketingApi,
|
MarketingApi,
|
||||||
SalesOrderApi,
|
SalesOrderApi,
|
||||||
@@ -33,6 +33,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
||||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
const RowsOptionsMenu = ({
|
const RowsOptionsMenu = ({
|
||||||
type = 'dropdown',
|
type = 'dropdown',
|
||||||
@@ -520,8 +521,53 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'latest_approval.step_name',
|
accessorKey: 'approval.step_name',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
|
cell: (props) => {
|
||||||
|
const approval = props.row.original.latest_approval;
|
||||||
|
const isRejected = approval?.action == 'REJECTED';
|
||||||
|
const isApproved = approval?.action == 'APPROVED';
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge:
|
||||||
|
'rounded-lg px-2 w-full flex flex-row justify-start whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
color={
|
||||||
|
isRejected
|
||||||
|
? 'error'
|
||||||
|
: isApproved
|
||||||
|
? approval?.step_number == 1
|
||||||
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:circle'
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
color={
|
||||||
|
approval?.step_number == 1
|
||||||
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isRejected
|
||||||
|
? 'Ditolak'
|
||||||
|
: formatTitleCase(approval?.step_name || '')}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'customer.name',
|
accessorKey: 'customer.name',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatDate,
|
formatDate,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
formatTitleCase,
|
||||||
formatVechicleNumber,
|
formatVechicleNumber,
|
||||||
} from '@/lib/helper';
|
} from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +35,7 @@ import toast from 'react-hot-toast';
|
|||||||
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
||||||
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
const MarketingDetail = ({
|
const MarketingDetail = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -121,6 +123,10 @@ const MarketingDetail = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const approval = initialValues?.latest_approval;
|
||||||
|
const isRejected = approval?.action == 'REJECTED';
|
||||||
|
const isApproved = approval?.action == 'APPROVED';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='flex flex-col w-full gap-4'>
|
<div className='flex flex-col w-full gap-4'>
|
||||||
@@ -230,7 +236,46 @@ const MarketingDetail = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='font-semibold'>Status</td>
|
<td className='font-semibold'>Status</td>
|
||||||
<td>:</td>
|
<td>:</td>
|
||||||
<td>{initialValues?.latest_approval?.step_name}</td>
|
<td>
|
||||||
|
<Badge
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge:
|
||||||
|
'rounded-lg px-2 w-fit flex flex-row justify-start whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
color={
|
||||||
|
isRejected
|
||||||
|
? 'error'
|
||||||
|
: isApproved
|
||||||
|
? approval?.step_number == 1
|
||||||
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:circle'
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
color={
|
||||||
|
approval?.step_number == 1
|
||||||
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isRejected
|
||||||
|
? 'Ditolak'
|
||||||
|
: formatTitleCase(approval?.step_name || '')}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='font-semibold'>Tanggal Penjualan</td>
|
<td className='font-semibold'>Tanggal Penjualan</td>
|
||||||
|
|||||||
@@ -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(
|
||||||
const currentProducts = formik.values.sales_order;
|
(id: number) => {
|
||||||
formik.setFieldValue(
|
const currentProducts = formik.values.sales_order;
|
||||||
'sales_order',
|
formik.setFieldValue(
|
||||||
currentProducts.filter((p) => p.id != id)
|
'sales_order',
|
||||||
);
|
currentProducts.filter((p) => p.id != id)
|
||||||
}, []);
|
);
|
||||||
|
},
|
||||||
|
[memoSalesOrder]
|
||||||
|
);
|
||||||
|
const handleEditSO = useCallback(
|
||||||
|
(id: number) => {
|
||||||
|
const currentProducts = formik.values.sales_order;
|
||||||
|
const selectedProduct = currentProducts.find((p) => p.id == id);
|
||||||
|
setSelectedMarketingProduct(selectedProduct ?? null);
|
||||||
|
addSOModal.openModal();
|
||||||
|
},
|
||||||
|
[memoSalesOrder]
|
||||||
|
);
|
||||||
const handleBulkDeleteSO = useCallback(() => {
|
const handleBulkDeleteSO = useCallback(() => {
|
||||||
const currentProducts = formik.values.sales_order;
|
const currentProducts = formik.values.sales_order;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
@@ -487,7 +501,7 @@ const MarketingForm = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
setRowSOSelection({});
|
setRowSOSelection({});
|
||||||
}, [selectedRowSOIds]);
|
}, [selectedRowSOIds, memoSalesOrder]);
|
||||||
const handleAddSOClick = useCallback(() => {
|
const handleAddSOClick = useCallback(() => {
|
||||||
setSelectedMarketingProduct(null);
|
setSelectedMarketingProduct(null);
|
||||||
addSOModal.openModal();
|
addSOModal.openModal();
|
||||||
@@ -523,7 +537,7 @@ const MarketingForm = ({
|
|||||||
|
|
||||||
addSOModal.closeModal();
|
addSOModal.closeModal();
|
||||||
},
|
},
|
||||||
[addSOModal]
|
[addSOModal, memoSalesOrder]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ================== DELIVERY ORDER HANDLER ==================
|
// ================== DELIVERY ORDER HANDLER ==================
|
||||||
@@ -568,8 +582,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);
|
||||||
@@ -621,7 +657,9 @@ const MarketingForm = ({
|
|||||||
isClearable
|
isClearable
|
||||||
placeholder='Pilih Pelanggan'
|
placeholder='Pilih Pelanggan'
|
||||||
isDisabled={
|
isDisabled={
|
||||||
formType === 'add_deliver' || formType === 'edit_deliver'
|
formType === 'add_deliver' ||
|
||||||
|
formType === 'edit_deliver' ||
|
||||||
|
formType === 'edit'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DateInput
|
<DateInput
|
||||||
@@ -652,6 +690,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}
|
||||||
/>
|
/>
|
||||||
@@ -671,6 +710,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<
|
||||||
|
|||||||
@@ -61,16 +61,17 @@ 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) => {
|
||||||
@@ -220,7 +221,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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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,16 +148,29 @@ 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
|
<>
|
||||||
color='warning'
|
<Button
|
||||||
className='px-2 py-1 text-sm'
|
color='warning'
|
||||||
onClick={() =>
|
className='px-2 py-1 text-sm'
|
||||||
onEditRef.current(props.row.original.id as number)
|
onClick={() =>
|
||||||
}
|
onEditRef.current(props.row.original.id as number)
|
||||||
type='button'
|
}
|
||||||
>
|
type='button'
|
||||||
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
>
|
||||||
</Button>
|
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='error'
|
||||||
|
className='px-2 py-1 text-sm'
|
||||||
|
onClick={() =>
|
||||||
|
onDeleteRef.current(props.row.original.id as number)
|
||||||
|
}
|
||||||
|
type='button'
|
||||||
|
disabled={!!props.row.original.do_number}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:delete' width={16} height={16} /> Hapus
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{!props.row.original.qty && '-'}
|
{!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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
<RowDropdownOptions isLast2Rows>
|
<div className='flex items-center gap-2'>
|
||||||
<PenerimaanBarangDropdown
|
{goodsReceiptItems[0]?.travel_document_path && (
|
||||||
onEdit={penerimaanBarangModal.openModal}
|
<Button
|
||||||
/>
|
color='primary'
|
||||||
</RowDropdownOptions>
|
className='w-fit min-w-32 flex items-center justify-start gap-1 p-1.5 text-sm'
|
||||||
|
href={goodsReceiptItems[0].travel_document_path}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:file-open-outline'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Lihat Dokumen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<RowDropdownOptions isLast2Rows>
|
||||||
|
<PenerimaanBarangDropdown
|
||||||
|
onEdit={penerimaanBarangModal.openModal}
|
||||||
|
/>
|
||||||
|
</RowDropdownOptions>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
|
import { createDashboardFilterSlice } from '@/stores/dashboard/slices/dashboard-filter.slice';
|
||||||
|
import { DashboardFilterSlice } from '@/types/stores';
|
||||||
|
|
||||||
|
export type DashboardStore = DashboardFilterSlice;
|
||||||
|
|
||||||
|
export const useDashboardStore = create<DashboardStore>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(...args) => ({
|
||||||
|
...createDashboardFilterSlice(...args),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'dashboard-filter-cache',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
name: 'DashboardStore',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useDashboardStore } from './dashboard.store';
|
||||||
|
export type { DashboardStore } from './dashboard.store';
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { DashboardFilterSlice } from '@/types/stores';
|
||||||
|
import { StateCreator } from 'zustand';
|
||||||
|
|
||||||
|
export const createDashboardFilterSlice: StateCreator<
|
||||||
|
DashboardFilterSlice,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
DashboardFilterSlice
|
||||||
|
> = (set) => ({
|
||||||
|
// Initial state
|
||||||
|
filterValues: {
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
analysisMode: 'OVERVIEW',
|
||||||
|
comparisonType: undefined,
|
||||||
|
location: [],
|
||||||
|
locationIds: undefined,
|
||||||
|
flock: undefined,
|
||||||
|
flockIds: undefined,
|
||||||
|
kandang: undefined,
|
||||||
|
kandangIds: undefined,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setFilterValues: (values) => set({ filterValues: values }),
|
||||||
|
|
||||||
|
resetFilterValues: () => {
|
||||||
|
return set({
|
||||||
|
filterValues: {
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
analysisMode: 'OVERVIEW',
|
||||||
|
comparisonType: undefined,
|
||||||
|
location: [],
|
||||||
|
locationIds: undefined,
|
||||||
|
flock: undefined,
|
||||||
|
flockIds: undefined,
|
||||||
|
kandang: undefined,
|
||||||
|
kandangIds: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
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;
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ export interface Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DashboardComparisonCharts {
|
export interface DashboardComparisonCharts {
|
||||||
location: DashboardCharts;
|
farm: DashboardCharts;
|
||||||
flock: DashboardCharts;
|
flock: DashboardCharts;
|
||||||
kandang: DashboardCharts;
|
kandang: DashboardCharts;
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
|||||||
Vendored
+11
@@ -1,3 +1,4 @@
|
|||||||
|
import { DashboardFilterType } from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
||||||
import type { ProductionStandardRepeaterFormSchemaValues } from '@/components/pages/master-data/production-standard/form/ProductionStandardForm.schema';
|
import type { ProductionStandardRepeaterFormSchemaValues } from '@/components/pages/master-data/production-standard/form/ProductionStandardForm.schema';
|
||||||
import type {
|
import type {
|
||||||
UniformityFormData,
|
UniformityFormData,
|
||||||
@@ -70,3 +71,13 @@ export type UniformitySlice = {
|
|||||||
setCreatedUniformity: (data: UniformityDetail | null) => void;
|
setCreatedUniformity: (data: UniformityDetail | null) => void;
|
||||||
resetUniformity: () => void;
|
resetUniformity: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Dashboard Filter Slice
|
||||||
|
export type DashboardFilterSlice = {
|
||||||
|
// State
|
||||||
|
filterValues: DashboardFilterType;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setFilterValues: (values: DashboardFilterType) => void;
|
||||||
|
resetFilterValues: () => void;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user