mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into fix/marketing
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",
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ const FinanceDetailPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(finance);
|
|
||||||
|
|
||||||
// if (!finance || isResponseError(finance)) {
|
// if (!finance || isResponseError(finance)) {
|
||||||
// router.replace('/404');
|
// router.replace('/404');
|
||||||
// return;
|
// return;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Button, { ButtonProps } from '@/components/Button';
|
import Button, { ButtonProps } from '@/components/Button';
|
||||||
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { FormikValues } from 'formik';
|
import { FormikValues } from 'formik';
|
||||||
|
|
||||||
@@ -13,11 +14,12 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
|||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={
|
className={cn(
|
||||||
getFilledFormikValuesCount(values) > 0
|
getFilledFormikValuesCount(values) > 0
|
||||||
? 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
|
? 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
|
||||||
: ''
|
: '',
|
||||||
}
|
props.className
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon='heroicons:funnel'
|
icon='heroicons:funnel'
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const AlertErrorList = ({
|
|||||||
if (formErrorList.length === 0) return null;
|
if (formErrorList.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert color='error' className='w-full flex flex-col gap-2 px-4 m-4'>
|
<Alert color='error' className='w-full flex flex-col gap-2 px-4'>
|
||||||
<div className='flex justify-between items-center gap-2 w-full'>
|
<div className='flex justify-between items-center gap-2 w-full'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Icon icon='material-symbols:error-outline' width={24} height={24} />
|
<Icon icon='material-symbols:error-outline' width={24} height={24} />
|
||||||
|
|||||||
@@ -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') : '';
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useSelect = <T,>(
|
const useSelect = <T,>(
|
||||||
basePath: string,
|
basePath: string | null,
|
||||||
valueKey: keyof T | string,
|
valueKey: keyof T | string,
|
||||||
labelKey: keyof T | string,
|
labelKey: keyof T | string,
|
||||||
searchKey: string = 'search',
|
searchKey: string = 'search',
|
||||||
@@ -354,7 +354,7 @@ const useSelect = <T,>(
|
|||||||
[limitKey]: String(limit),
|
[limitKey]: String(limit),
|
||||||
}).toString();
|
}).toString();
|
||||||
|
|
||||||
return `${basePath}?${qs}`;
|
return basePath ? `${basePath}?${qs}` : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -3,224 +3,82 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import {
|
import { HppItem, ProfitLossItem } from '@/types/api/closing';
|
||||||
DataSummarySubTotal,
|
import { useSearchParams } from 'next/navigation';
|
||||||
HppPurchaseData,
|
import { useMemo } from 'react';
|
||||||
ProfitLossDataAmount,
|
|
||||||
} from '@/types/api/closing';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
type HppTableRow =
|
|
||||||
| (HppPurchaseData & {
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader?: boolean;
|
|
||||||
})
|
|
||||||
| {
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader: true;
|
|
||||||
type?: never;
|
|
||||||
budgeting?: never;
|
|
||||||
realization?: never;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: string;
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader: false;
|
|
||||||
budgeting?: { rp_per_bird: number; rp_per_kg: number; amount: number };
|
|
||||||
realization?: { rp_per_bird: number; rp_per_kg: number; amount: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProfitLossTableRow =
|
|
||||||
| (DataSummarySubTotal & {
|
|
||||||
type: string;
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader?: boolean;
|
|
||||||
})
|
|
||||||
| {
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader: true;
|
|
||||||
type?: never;
|
|
||||||
rp_per_bird?: never;
|
|
||||||
rp_per_kg?: never;
|
|
||||||
amount?: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ClosingFinanceTable = ({
|
const ClosingFinanceTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: {
|
}: {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const { data: finance, isLoading } = useSWR(
|
const { data: finance, isLoading } = useSWR(
|
||||||
`/closing/finance/${projectFlockId}`,
|
`/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
||||||
() => ClosingApi.getFinance(projectFlockId)
|
() =>
|
||||||
|
ClosingApi.getFinance(
|
||||||
|
projectFlockId,
|
||||||
|
kandangId ? Number(kandangId) : undefined
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const staticHppRows: Array<{
|
const hppTableData: HppItem[] = useMemo(() => {
|
||||||
group_name: string;
|
if (isResponseSuccess(finance)) {
|
||||||
type: string;
|
const customItems = {
|
||||||
group_index: number;
|
label: 'HPP dan Pengeluaran',
|
||||||
}> = [
|
code: 'custom_row',
|
||||||
{
|
} as HppItem;
|
||||||
group_name: 'HPP dan Pengeluaran',
|
const purchases = finance.data.hpp.items.filter(
|
||||||
type: 'Pembelian PAKAN',
|
(item) => item.category === 'purchase'
|
||||||
group_index: 0,
|
);
|
||||||
},
|
const totalBudgeting = {
|
||||||
{
|
label: 'HPP dan Bahan Baku',
|
||||||
group_name: 'HPP dan Pengeluaran',
|
code: 'custom_row',
|
||||||
type: 'Pembelian STARTER',
|
} as HppItem;
|
||||||
group_index: 0,
|
const overheads = finance.data.hpp.items.filter(
|
||||||
},
|
(item) => item.category === 'overhead'
|
||||||
{
|
);
|
||||||
group_name: 'HPP dan Pengeluaran',
|
return [customItems, ...purchases, totalBudgeting, ...overheads];
|
||||||
type: 'Pembelian DOC',
|
}
|
||||||
group_index: 0,
|
return [];
|
||||||
},
|
}, [finance]);
|
||||||
{
|
|
||||||
group_name: 'HPP dan Pengeluaran',
|
|
||||||
type: 'Pembelian PULLET',
|
|
||||||
group_index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group_name: 'HPP dan Pengeluaran',
|
|
||||||
type: 'Pembelian LAYER',
|
|
||||||
group_index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group_name: 'HPP dan Bahan Baku',
|
|
||||||
type: 'Pengeluaran Overhead',
|
|
||||||
group_index: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group_name: 'HPP dan Bahan Baku',
|
|
||||||
type: 'Beban Ekspedisi',
|
|
||||||
group_index: 1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const hppTableData: HppTableRow[] = [
|
const profitLossTableData: ProfitLossItem[] = useMemo(() => {
|
||||||
{
|
if (isResponseSuccess(finance)) {
|
||||||
group_name: 'HPP dan Pengeluaran',
|
const incomes = finance.data.profit_loss.items.filter(
|
||||||
group_index: 0,
|
(item) => item.type === 'income'
|
||||||
isGroupHeader: true as const,
|
);
|
||||||
},
|
const purchases = finance.data.profit_loss.items.filter(
|
||||||
...staticHppRows
|
(item) => item.type === 'purchase'
|
||||||
.filter((row) => row.group_index === 0)
|
);
|
||||||
.map((staticRow) => {
|
const overheads = finance.data.profit_loss.items.filter(
|
||||||
const apiData = isResponseSuccess(finance)
|
(item) => item.type === 'overhead'
|
||||||
? finance.data.hpp_purchases.hpp
|
);
|
||||||
.find((g) => g.group_name === staticRow.group_name)
|
const grossProfit = {
|
||||||
?.data.find((d) => d.type === staticRow.type)
|
label: 'LABA RUGI BRUTO',
|
||||||
: null;
|
code: 'custom_row',
|
||||||
|
type: 'gross_profit',
|
||||||
return {
|
rp_per_bird:
|
||||||
group_name: staticRow.group_name,
|
finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
|
||||||
group_index: staticRow.group_index,
|
rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
|
||||||
type: staticRow.type,
|
amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
|
||||||
budgeting: apiData?.budgeting || {
|
} as ProfitLossItem;
|
||||||
rp_per_bird: 0,
|
const subtotal = {
|
||||||
rp_per_kg: 0,
|
label: 'Subtotal',
|
||||||
amount: 0,
|
code: 'custom_row',
|
||||||
},
|
type: 'subtotal',
|
||||||
realization: apiData?.realization || {
|
rp_per_bird:
|
||||||
rp_per_bird: 0,
|
finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
|
||||||
rp_per_kg: 0,
|
rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
|
||||||
amount: 0,
|
amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
|
||||||
},
|
} as ProfitLossItem;
|
||||||
isGroupHeader: false as const,
|
return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
|
||||||
};
|
}
|
||||||
}),
|
return [];
|
||||||
{
|
}, [finance]);
|
||||||
group_name: 'HPP dan Bahan Baku',
|
|
||||||
group_index: 1,
|
|
||||||
isGroupHeader: true as const,
|
|
||||||
},
|
|
||||||
...staticHppRows
|
|
||||||
.filter((row) => row.group_index === 1)
|
|
||||||
.map((staticRow) => {
|
|
||||||
const apiData = isResponseSuccess(finance)
|
|
||||||
? finance.data.hpp_purchases.hpp
|
|
||||||
.find((g) => g.group_name === staticRow.group_name)
|
|
||||||
?.data.find((d) => d.type === staticRow.type)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
group_name: staticRow.group_name,
|
|
||||||
group_index: staticRow.group_index,
|
|
||||||
type: staticRow.type,
|
|
||||||
budgeting: apiData?.budgeting || {
|
|
||||||
rp_per_bird: 0,
|
|
||||||
rp_per_kg: 0,
|
|
||||||
amount: 0,
|
|
||||||
},
|
|
||||||
realization: apiData?.realization || {
|
|
||||||
rp_per_bird: 0,
|
|
||||||
rp_per_kg: 0,
|
|
||||||
amount: 0,
|
|
||||||
},
|
|
||||||
isGroupHeader: false as const,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
group_name: 'HPP',
|
|
||||||
group_index: 2,
|
|
||||||
isGroupHeader: true as const,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance)
|
|
||||||
? [
|
|
||||||
// Pembelian group
|
|
||||||
...finance.data.profit_loss.data.pembelian.map((item) => ({
|
|
||||||
label: 'Pembelian',
|
|
||||||
group_name: 'Pembelian',
|
|
||||||
group_index: 1,
|
|
||||||
type: item.type,
|
|
||||||
rp_per_bird: item.rp_per_bird,
|
|
||||||
rp_per_kg: item.rp_per_kg,
|
|
||||||
amount: item.amount,
|
|
||||||
isGroupHeader: false as const,
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
label: finance.data.profit_loss.data.summary.gross_profit.label,
|
|
||||||
group_name: 'Penjualan',
|
|
||||||
group_index: 0,
|
|
||||||
isGroupHeader: true as const,
|
|
||||||
type: finance.data.profit_loss.data.summary.gross_profit.label,
|
|
||||||
rp_per_bird:
|
|
||||||
finance.data.profit_loss.data.summary.gross_profit.rp_per_bird,
|
|
||||||
rp_per_kg:
|
|
||||||
finance.data.profit_loss.data.summary.gross_profit.rp_per_kg,
|
|
||||||
amount: finance.data.profit_loss.data.summary.gross_profit.amount,
|
|
||||||
},
|
|
||||||
// Penjualan group
|
|
||||||
...finance.data.profit_loss.data.penjualan.map((item) => ({
|
|
||||||
label: 'Penjualan',
|
|
||||||
group_name: 'Penjualan',
|
|
||||||
group_index: 0,
|
|
||||||
type: item.type,
|
|
||||||
rp_per_bird: item.rp_per_bird,
|
|
||||||
rp_per_kg: item.rp_per_kg,
|
|
||||||
amount: item.amount,
|
|
||||||
isGroupHeader: false as const,
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
label: finance.data.profit_loss.data.summary.sub_total.label,
|
|
||||||
group_name: 'Pembelian',
|
|
||||||
group_index: 1,
|
|
||||||
isGroupHeader: true as const,
|
|
||||||
type: finance.data.profit_loss.data.summary.sub_total.label,
|
|
||||||
rp_per_bird:
|
|
||||||
finance.data.profit_loss.data.summary.sub_total.rp_per_bird,
|
|
||||||
rp_per_kg: finance.data.profit_loss.data.summary.sub_total.rp_per_kg,
|
|
||||||
amount: finance.data.profit_loss.data.summary.sub_total.amount,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
@@ -233,35 +91,21 @@ const ClosingFinanceTable = ({
|
|||||||
>
|
>
|
||||||
<div className='grid grid-cols-2 gap-6'>
|
<div className='grid grid-cols-2 gap-6'>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div>
|
<div>Laba Rugi Brutto</div>
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatTitleCase(
|
|
||||||
finance.data.profit_loss.data.summary.gross_profit
|
|
||||||
.label || '-'
|
|
||||||
)
|
|
||||||
: 'Laba Rugi Brutto'}
|
|
||||||
</div>
|
|
||||||
<div className='text-lg font-bold'>
|
<div className='text-lg font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.gross_profit.amount
|
finance.data.profit_loss.summary.gross_profit.amount
|
||||||
)
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div>
|
<div>Laba Rugi Netto</div>
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatTitleCase(
|
|
||||||
finance.data.profit_loss.data.summary.net_profit.label ||
|
|
||||||
'-'
|
|
||||||
)
|
|
||||||
: 'Laba Rugi Netto'}
|
|
||||||
</div>
|
|
||||||
<div className='text-lg font-bold'>
|
<div className='text-lg font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.net_profit.amount
|
finance.data.profit_loss.summary.net_profit.amount
|
||||||
)
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
@@ -269,11 +113,7 @@ const ClosingFinanceTable = ({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
title={
|
title='HPP Purchases'
|
||||||
isResponseSuccess(finance)
|
|
||||||
? finance.data.hpp_purchases.title
|
|
||||||
: 'HPP Purchases'
|
|
||||||
}
|
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
collapsible
|
collapsible
|
||||||
className={{
|
className={{
|
||||||
@@ -281,17 +121,18 @@ const ClosingFinanceTable = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='mt-6 p-0 mb-0'>
|
<div className='mt-6 p-0 mb-0'>
|
||||||
<Table<HppTableRow>
|
<Table<HppItem>
|
||||||
data={hppTableData}
|
data={hppTableData}
|
||||||
|
isLoading={isLoading}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'No.',
|
header: 'No.',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item, index) => {
|
accessorFn: (item, index) => {
|
||||||
if (item.isGroupHeader) return '-';
|
if (item.code === 'custom_row') return '-';
|
||||||
const dataRowsBefore = hppTableData
|
const dataRowsBefore = hppTableData
|
||||||
.slice(0, index)
|
.slice(0, index)
|
||||||
.filter((row) => !row.isGroupHeader).length;
|
.filter((row) => row.code !== 'custom_row').length;
|
||||||
return dataRowsBefore + 1;
|
return dataRowsBefore + 1;
|
||||||
},
|
},
|
||||||
footer: (props) => {
|
footer: (props) => {
|
||||||
@@ -301,7 +142,7 @@ const ClosingFinanceTable = ({
|
|||||||
{
|
{
|
||||||
header: 'Jenis',
|
header: 'Jenis',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => formatTitleCase(item.type || '-'),
|
accessorFn: (item) => formatTitleCase(item.label || '-'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Budgeting',
|
header: 'Budgeting',
|
||||||
@@ -317,7 +158,7 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'budgeting_rp_per_bird' &&
|
return props.column.id === 'budgeting_rp_per_bird' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp?.budgeting
|
finance.data.hpp.summary?.budgeting
|
||||||
?.rp_per_bird || 0
|
?.rp_per_bird || 0
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
@@ -333,8 +174,8 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'budgeting_rp_per_kg' &&
|
return props.column.id === 'budgeting_rp_per_kg' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp?.budgeting
|
finance.data.hpp.summary?.budgeting?.rp_per_kg ||
|
||||||
?.rp_per_kg || 0
|
0
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -349,8 +190,7 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'budgeting_amount' &&
|
return props.column.id === 'budgeting_amount' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp?.budgeting
|
finance.data.hpp.summary?.budgeting?.amount || 0
|
||||||
?.amount || 0
|
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -371,8 +211,8 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'realization_rp_per_bird' &&
|
return props.column.id === 'realization_rp_per_bird' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp
|
finance.data.hpp.summary?.realization
|
||||||
?.realization?.rp_per_bird || 0
|
?.rp_per_bird || 0
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -387,8 +227,8 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'realization_rp_per_kg' &&
|
return props.column.id === 'realization_rp_per_kg' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp
|
finance.data.hpp.summary?.realization
|
||||||
?.realization?.rp_per_kg || 0
|
?.rp_per_kg || 0
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -403,8 +243,7 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'realization_amount' &&
|
return props.column.id === 'realization_amount' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp
|
finance.data.hpp.summary?.realization?.amount || 0
|
||||||
?.realization?.amount || 0
|
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -414,7 +253,7 @@ const ClosingFinanceTable = ({
|
|||||||
]}
|
]}
|
||||||
renderCustomRow={(row) => {
|
renderCustomRow={(row) => {
|
||||||
const rowData = row.original;
|
const rowData = row.original;
|
||||||
if (rowData.isGroupHeader) {
|
if (rowData.code === 'custom_row') {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={row.id}
|
key={row.id}
|
||||||
@@ -428,7 +267,7 @@ const ClosingFinanceTable = ({
|
|||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
>
|
>
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{formatTitleCase(rowData.group_name ?? '-')}
|
{formatTitleCase(rowData.label ?? '-')}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -441,11 +280,7 @@ const ClosingFinanceTable = ({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
title={
|
title='Profit/Loss'
|
||||||
isResponseSuccess(finance)
|
|
||||||
? finance.data.profit_loss.title
|
|
||||||
: 'Profit/Loss'
|
|
||||||
}
|
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
collapsible
|
collapsible
|
||||||
className={{
|
className={{
|
||||||
@@ -453,38 +288,32 @@ const ClosingFinanceTable = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='mt-6 p-0 mb-0'>
|
<div className='mt-6 p-0 mb-0'>
|
||||||
<Table<ProfitLossTableRow>
|
<Table<ProfitLossItem>
|
||||||
data={profitLossTableData}
|
data={profitLossTableData}
|
||||||
|
isLoading={isLoading}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'Jenis',
|
header: 'Jenis',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => item.type,
|
accessorFn: (item) => item.label,
|
||||||
cell: (item) => (
|
cell: (item) => (
|
||||||
<div className=''>
|
<div className=''>
|
||||||
{formatTitleCase(item.row.original.type || '-')}
|
{formatTitleCase(item.row.original.label || '-')}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
footer: (item) => (
|
footer: () => (
|
||||||
<div className='font-bold uppercase'>
|
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatTitleCase(
|
|
||||||
finance.data.profit_loss.data.summary.net_profit
|
|
||||||
.label || '-'
|
|
||||||
)
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Rp/Ekor',
|
header: 'Rp/Ekor',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
||||||
footer: (item) => (
|
footer: () => (
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.net_profit
|
finance.data.profit_loss.summary.net_profit
|
||||||
.rp_per_bird || 0
|
.rp_per_bird || 0
|
||||||
)
|
)
|
||||||
: formatCurrency(0)}
|
: formatCurrency(0)}
|
||||||
@@ -495,11 +324,11 @@ const ClosingFinanceTable = ({
|
|||||||
header: 'Rp/Kg',
|
header: 'Rp/Kg',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
||||||
footer: (item) => (
|
footer: () => (
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.net_profit
|
finance.data.profit_loss.summary.net_profit
|
||||||
.rp_per_kg || 0
|
.rp_per_kg || 0
|
||||||
)
|
)
|
||||||
: formatCurrency(0)}
|
: formatCurrency(0)}
|
||||||
@@ -510,11 +339,11 @@ const ClosingFinanceTable = ({
|
|||||||
header: 'Jumlah (Rp)',
|
header: 'Jumlah (Rp)',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => formatCurrency(item.amount || 0),
|
accessorFn: (item) => formatCurrency(item.amount || 0),
|
||||||
footer: (item) => (
|
footer: () => (
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.net_profit
|
finance.data.profit_loss.summary.net_profit
|
||||||
.amount || 0
|
.amount || 0
|
||||||
)
|
)
|
||||||
: formatCurrency(0)}
|
: formatCurrency(0)}
|
||||||
@@ -524,55 +353,30 @@ const ClosingFinanceTable = ({
|
|||||||
]}
|
]}
|
||||||
renderCustomRow={(row) => {
|
renderCustomRow={(row) => {
|
||||||
const rowData = row.original;
|
const rowData = row.original;
|
||||||
if (rowData.isGroupHeader) {
|
if (rowData.code === 'custom_row') {
|
||||||
if (rowData.amount) {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={row.id}
|
|
||||||
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
>
|
|
||||||
<div className='font-bold ps-6 uppercase'>
|
|
||||||
{formatTitleCase(rowData.label ?? '-')}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatCurrency(rowData.amount ?? 0)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
||||||
>
|
>
|
||||||
<td
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
colSpan={4}
|
<div className='font-bold ps-6 uppercase'>
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
{formatTitleCase(rowData.label ?? '-')}
|
||||||
>
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{formatTitleCase(rowData.group_name ?? '-')}
|
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.amount ?? 0)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ interface ClosingIncomingSapronaksTableProps {
|
|||||||
const ClosingIncomingSapronaksTable = ({
|
const ClosingIncomingSapronaksTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingIncomingSapronaksTableProps) => {
|
}: ClosingIncomingSapronaksTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -43,7 +47,7 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
|
|
||||||
const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } =
|
const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } =
|
||||||
useSWR(
|
useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming`,
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
ClosingApi.getAllIncomingSapronakFetcher,
|
ClosingApi.getAllIncomingSapronakFetcher,
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ interface ClosingOutgoingSapronaksTableProps {
|
|||||||
const ClosingOutgoingSapronaksTable = ({
|
const ClosingOutgoingSapronaksTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingOutgoingSapronaksTableProps) => {
|
}: ClosingOutgoingSapronaksTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -43,7 +47,7 @@ const ClosingOutgoingSapronaksTable = ({
|
|||||||
|
|
||||||
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
||||||
useSWR(
|
useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing`,
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
ClosingApi.getAllOutgoingSapronakFetcher,
|
ClosingApi.getAllOutgoingSapronakFetcher,
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
|
|||||||
@@ -32,101 +32,160 @@ const ClosingOverheadTable = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Helper function to create columns with footer support
|
// Helper function to create columns with footer support
|
||||||
const createColumns = (total?: OverheadTotal): ColumnDef<Overhead>[] => [
|
const createColumns = (
|
||||||
// Group untuk kolom tanpa footer
|
total?: OverheadTotal,
|
||||||
{
|
kandangId?: number
|
||||||
header: 'Nama Item',
|
): ColumnDef<Overhead>[] => {
|
||||||
accessorFn: (props) => props.item_name,
|
const flockColumn: ColumnDef<Overhead>[] = [
|
||||||
footer: 'Total Pengeluaran Overhead',
|
{
|
||||||
},
|
header: 'Budget Pengajuan',
|
||||||
{
|
footer: '',
|
||||||
header: 'Satuan',
|
columns: [
|
||||||
accessorFn: (props) => props.uom_name,
|
{
|
||||||
},
|
id: 'budget_quantity',
|
||||||
{
|
header: 'Jumlah',
|
||||||
header: 'Budget Pengajuan',
|
accessorFn: (props) =>
|
||||||
footer: '',
|
props.budget_quantity ? formatNumber(props.budget_quantity) : '-',
|
||||||
columns: [
|
footer: total ? () => formatNumber(total.budget_quantity) : '',
|
||||||
{
|
},
|
||||||
id: 'budget_quantity',
|
{
|
||||||
header: 'Jumlah',
|
id: 'budget_unit_price',
|
||||||
accessorFn: (props) =>
|
header: 'Harga Satuan',
|
||||||
props.budget_quantity ? formatNumber(props.budget_quantity) : '-',
|
accessorFn: (props) =>
|
||||||
footer: total ? () => formatNumber(total.budget_quantity) : '',
|
props.budget_unit_price
|
||||||
},
|
? formatCurrency(props.budget_unit_price)
|
||||||
{
|
: '-',
|
||||||
id: 'budget_unit_price',
|
footer: '',
|
||||||
header: 'Harga Satuan',
|
},
|
||||||
accessorFn: (props) =>
|
{
|
||||||
props.budget_unit_price
|
id: 'budget_total_amount',
|
||||||
? formatCurrency(props.budget_unit_price)
|
header: 'Total',
|
||||||
: '-',
|
accessorFn: (props) =>
|
||||||
footer: '',
|
props.budget_total_amount
|
||||||
},
|
? formatCurrency(props.budget_total_amount)
|
||||||
{
|
: '-',
|
||||||
id: 'budget_total_amount',
|
footer: total
|
||||||
header: 'Total',
|
? () => formatCurrency(total.budget_total_amount)
|
||||||
accessorFn: (props) =>
|
: '',
|
||||||
props.budget_total_amount
|
},
|
||||||
? formatCurrency(props.budget_total_amount)
|
],
|
||||||
: '-',
|
},
|
||||||
footer: total ? () => formatCurrency(total.budget_total_amount) : '',
|
{
|
||||||
},
|
header: 'Realisasi',
|
||||||
],
|
footer: '',
|
||||||
},
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Realisasi',
|
id: 'actual_date',
|
||||||
footer: '',
|
header: 'Tanggal',
|
||||||
columns: [
|
accessorFn: (props) =>
|
||||||
{
|
props.actual_date
|
||||||
id: 'actual_date',
|
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
||||||
header: 'Tanggal',
|
: '-',
|
||||||
accessorFn: (props) =>
|
footer: '',
|
||||||
props.actual_date
|
},
|
||||||
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
{
|
||||||
: '-',
|
id: 'actual_quantity',
|
||||||
footer: '',
|
header: 'Jumlah',
|
||||||
},
|
accessorFn: (props) =>
|
||||||
{
|
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
||||||
id: 'actual_quantity',
|
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
||||||
header: 'Jumlah',
|
},
|
||||||
accessorFn: (props) =>
|
{
|
||||||
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
id: 'actual_unit_price',
|
||||||
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
header: 'Harga Satuan',
|
||||||
},
|
accessorFn: (props) =>
|
||||||
{
|
props.actual_unit_price
|
||||||
id: 'actual_unit_price',
|
? formatCurrency(props.actual_unit_price)
|
||||||
header: 'Harga Satuan',
|
: '-',
|
||||||
accessorFn: (props) =>
|
footer: '',
|
||||||
props.actual_unit_price
|
},
|
||||||
? formatCurrency(props.actual_unit_price)
|
{
|
||||||
: '-',
|
id: 'actual_total_amount',
|
||||||
footer: '',
|
header: 'Total',
|
||||||
},
|
accessorFn: (props) =>
|
||||||
{
|
props.actual_total_amount
|
||||||
id: 'actual_total_amount',
|
? formatCurrency(props.actual_total_amount)
|
||||||
header: 'Total',
|
: '-',
|
||||||
accessorFn: (props) =>
|
footer: total
|
||||||
props.actual_total_amount
|
? () => formatCurrency(total.actual_total_amount)
|
||||||
? formatCurrency(props.actual_total_amount)
|
: '',
|
||||||
: '-',
|
},
|
||||||
footer: total ? () => formatCurrency(total.actual_total_amount) : '',
|
],
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
},
|
|
||||||
{
|
const kandangColumn: ColumnDef<Overhead>[] = [
|
||||||
id: 'cost_per_bird',
|
{
|
||||||
header: 'Rp/Ekor',
|
id: 'actual_date',
|
||||||
accessorFn: (props) =>
|
header: 'Tanggal',
|
||||||
props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-',
|
accessorFn: (props) =>
|
||||||
footer: total ? () => formatCurrency(total.cost_per_bird) : '',
|
props.actual_date
|
||||||
},
|
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
||||||
];
|
: '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actual_quantity',
|
||||||
|
header: 'Jumlah',
|
||||||
|
accessorFn: (props) =>
|
||||||
|
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
||||||
|
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actual_unit_price',
|
||||||
|
header: 'Harga Satuan',
|
||||||
|
accessorFn: (props) =>
|
||||||
|
props.actual_unit_price
|
||||||
|
? formatCurrency(props.actual_unit_price)
|
||||||
|
: '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actual_total_amount',
|
||||||
|
header: 'Total',
|
||||||
|
accessorFn: (props) =>
|
||||||
|
props.actual_total_amount
|
||||||
|
? formatCurrency(props.actual_total_amount)
|
||||||
|
: '-',
|
||||||
|
footer: total ? () => formatCurrency(total.actual_total_amount) : '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const finalColumns: ColumnDef<Overhead>[] = [
|
||||||
|
// Group untuk kolom tanpa footer
|
||||||
|
{
|
||||||
|
header: 'No',
|
||||||
|
accessorFn: (_, index) => index,
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Nama Item',
|
||||||
|
accessorFn: (props) => props.item_name,
|
||||||
|
footer: 'Total Pengeluaran Overhead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Satuan',
|
||||||
|
accessorFn: (props) => props.uom_name,
|
||||||
|
},
|
||||||
|
...(kandangId ? kandangColumn : flockColumn),
|
||||||
|
{
|
||||||
|
id: 'cost_per_bird',
|
||||||
|
header: 'Rp/Ekor',
|
||||||
|
accessorFn: (props) =>
|
||||||
|
props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-',
|
||||||
|
footer: total ? () => formatCurrency(total.cost_per_bird) : '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return finalColumns;
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isResponseSuccess(overhead)
|
isResponseSuccess(overhead)
|
||||||
? createColumns(overhead.data?.total)
|
? createColumns(
|
||||||
|
overhead.data?.total,
|
||||||
|
kandangId ? Number(kandangId) : undefined
|
||||||
|
)
|
||||||
: createColumns(),
|
: createColumns(),
|
||||||
[overhead]
|
[overhead]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -12,9 +13,12 @@ interface ClosingProductionDataTabContentProps {
|
|||||||
const ClosingProductionDataTabContent = ({
|
const ClosingProductionDataTabContent = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingProductionDataTabContentProps) => {
|
}: ClosingProductionDataTabContentProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const { data: productionData, isLoading } = useSWR(
|
const { data: productionData, isLoading } = useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/production-data`,
|
`${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
() => ClosingApi.getProductionData(projectFlockId)
|
() => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ const ClosingsTable = () => {
|
|||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
@@ -228,6 +229,7 @@ const ClosingsTable = () => {
|
|||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6',
|
wrapper: 'col-span-12 sm:col-span-6',
|
||||||
|
|||||||
@@ -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 } 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,
|
||||||
@@ -30,6 +33,11 @@ import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
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 Dropdown from '@/components/Dropdown';
|
||||||
|
import Menu from '@/components/menu/Menu';
|
||||||
|
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 = (
|
||||||
@@ -44,11 +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 statsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const allChartsRef = useRef<DashboardAllChartsRef>(null);
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -64,22 +83,32 @@ const DashboardProduction = () => {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// ===== SELECT =====
|
// ===== SELECT =====
|
||||||
const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } =
|
|
||||||
useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
|
|
||||||
limit: 'limit',
|
|
||||||
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
|
||||||
});
|
|
||||||
const {
|
const {
|
||||||
|
setInputValue: setInputValueFlock,
|
||||||
|
options: flockOptions,
|
||||||
|
isLoadingOptions: isLoadingFlockOptions,
|
||||||
|
loadMore: loadMoreFlock,
|
||||||
|
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
|
||||||
|
limit: 'limit',
|
||||||
|
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
setInputValue: setInputValueLocation,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocation,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
limit: 'limit',
|
limit: 'limit',
|
||||||
});
|
});
|
||||||
const { options: kandangOptions, isLoadingOptions: isLoadingKandangOptions } =
|
const {
|
||||||
useSelect(KandangApi.basePath, 'id', 'name', '', {
|
setInputValue: setInputValueKandang,
|
||||||
limit: 'limit',
|
options: kandangOptions,
|
||||||
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
isLoadingOptions: isLoadingKandangOptions,
|
||||||
});
|
loadMore: loadMoreKandang,
|
||||||
|
} = useSelect(KandangApi.basePath, 'id', 'name', '', {
|
||||||
|
limit: 'limit',
|
||||||
|
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
||||||
|
});
|
||||||
const comparisonTypeOptions = [
|
const comparisonTypeOptions = [
|
||||||
{ value: 'FARM', label: 'Farm' },
|
{ value: 'FARM', label: 'Farm' },
|
||||||
{ value: 'FLOCK', label: 'Flock' },
|
{ value: 'FLOCK', label: 'Flock' },
|
||||||
@@ -89,20 +118,21 @@ 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) => {
|
||||||
console.log(values);
|
// Save filter values to store
|
||||||
|
setFilterValues(values);
|
||||||
|
|
||||||
handleApplyFilter({
|
handleApplyFilter({
|
||||||
start_date: values.startDate || '',
|
start_date: values.startDate || '',
|
||||||
@@ -118,13 +148,13 @@ 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) => {
|
||||||
console.log(values);
|
|
||||||
|
|
||||||
// Build query params object, only include non-empty values
|
// Build query params object, only include non-empty values
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -140,15 +170,37 @@ const DashboardProduction = () => {
|
|||||||
if (values.comparison_type) params.comparison_type = values.comparison_type;
|
if (values.comparison_type) params.comparison_type = values.comparison_type;
|
||||||
|
|
||||||
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
|
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
|
||||||
console.log(endpointUrl);
|
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
refreshDashboardProductionData();
|
refreshDashboardProductionData();
|
||||||
formik.resetForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== 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 =====
|
||||||
|
const handleExportPDF = async () => {
|
||||||
|
await generateDashboardPDF({
|
||||||
|
filterValues: formik.values,
|
||||||
|
statsRef,
|
||||||
|
allChartsRef,
|
||||||
|
setExporting,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
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'>
|
||||||
@@ -156,103 +208,108 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full p-4 space-y-6'>
|
<section className='w-full p-4 space-y-6'>
|
||||||
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
|
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-end gap-2'>
|
<div className='flex flex-row justify-end gap-2'>
|
||||||
<Button
|
<ButtonFilter
|
||||||
|
values={{
|
||||||
|
...formik.values,
|
||||||
|
analysisMode: undefined,
|
||||||
|
}}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className={`min-w-28 rounded-lg ${
|
|
||||||
isResponseSuccess(dashboardProductionResponse) &&
|
|
||||||
(dashboardProductionResponse.meta as unknown as DashboardMeta)
|
|
||||||
.filters
|
|
||||||
? 'bg-gradient-to-r from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
onClick={() => filterModal.openModal()}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:funnel'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className={
|
|
||||||
isResponseSuccess(dashboardProductionResponse) &&
|
|
||||||
(dashboardProductionResponse.meta as unknown as DashboardMeta)
|
|
||||||
.filters
|
|
||||||
? 'text-blue-600'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Filter
|
|
||||||
{isResponseSuccess(dashboardProductionResponse) &&
|
|
||||||
dashboardProductionResponse.meta &&
|
|
||||||
(dashboardProductionResponse.meta as unknown as DashboardMeta)
|
|
||||||
.filters && (
|
|
||||||
<span className='w-6 h-6 text-white bg-red-500 rounded-lg flex items-center justify-center text-xs'>
|
|
||||||
{(() => {
|
|
||||||
const meta =
|
|
||||||
dashboardProductionResponse.meta as unknown as DashboardMeta;
|
|
||||||
if (!meta.filters) return 0;
|
|
||||||
const count =
|
|
||||||
(meta.filters.location_ids.length > 1
|
|
||||||
? meta.filters.location_ids.length
|
|
||||||
: 0) +
|
|
||||||
(meta.filters.flock_ids.length > 1
|
|
||||||
? meta.filters.flock_ids.length
|
|
||||||
: 0) +
|
|
||||||
(meta.filters.kandang_ids.length > 1
|
|
||||||
? meta.filters.kandang_ids.length
|
|
||||||
: 0);
|
|
||||||
return meta.filters.analysis_mode === 'OVERVIEW'
|
|
||||||
? 1
|
|
||||||
: count;
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='neutral'
|
|
||||||
className='min-w-28 rounded-lg'
|
className='min-w-28 rounded-lg'
|
||||||
|
onClick={() => filterModal.openModal()}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
trigger={
|
||||||
|
<Button variant='outline' className='min-w-28 rounded-lg z-50'>
|
||||||
|
<Icon icon='heroicons:arrow-down-tray' />
|
||||||
|
Export
|
||||||
|
<Icon icon='heroicons:chevron-down' />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
content: 'w-full',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:arrow-down-tray' width={20} height={20} />
|
<Menu className={exporting ? 'hidden' : ''}>
|
||||||
Export
|
<MenuItem title='PDF' onClick={handleExportPDF} />
|
||||||
<Icon icon='heroicons:chevron-down' width={20} height={20} />
|
</Menu>
|
||||||
</Button>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
@@ -287,7 +344,7 @@ const DashboardProduction = () => {
|
|||||||
{/* Rentang Waktu */}
|
{/* Rentang Waktu */}
|
||||||
<div className='px-4'>
|
<div className='px-4'>
|
||||||
<label className='flex items-center gap-2 mb-3'>Tanggal</label>
|
<label className='flex items-center gap-2 mb-3'>Tanggal</label>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-start gap-2'>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='startDate'
|
name='startDate'
|
||||||
placeholder='Tanggal Mulai'
|
placeholder='Tanggal Mulai'
|
||||||
@@ -302,7 +359,7 @@ const DashboardProduction = () => {
|
|||||||
Boolean(formik.touched.startDate)
|
Boolean(formik.touched.startDate)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span className='hidden md:block text-center'>—</span>
|
<div className='hidden md:block mt-3 text-center'>—</div>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='endDate'
|
name='endDate'
|
||||||
placeholder='Tanggal Akhir'
|
placeholder='Tanggal Akhir'
|
||||||
@@ -383,6 +440,8 @@ const DashboardProduction = () => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
label='Farm'
|
label='Farm'
|
||||||
value={formik.values.location}
|
value={formik.values.location}
|
||||||
|
onInputChange={setInputValueLocation}
|
||||||
|
onMenuScrollToBottom={loadMoreLocation}
|
||||||
onChange={(selected) => {
|
onChange={(selected) => {
|
||||||
formik.setFieldValue('location', selected);
|
formik.setFieldValue('location', selected);
|
||||||
// Update selectedLocationIds for kandang filter
|
// Update selectedLocationIds for kandang filter
|
||||||
@@ -422,6 +481,8 @@ const DashboardProduction = () => {
|
|||||||
formik.setFieldValue('flock', selected)
|
formik.setFieldValue('flock', selected)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.flock as string}
|
errorMessage={formik.errors.flock as string}
|
||||||
|
onInputChange={setInputValueFlock}
|
||||||
|
onMenuScrollToBottom={loadMoreFlock}
|
||||||
options={flockOptions}
|
options={flockOptions}
|
||||||
isLoading={isLoadingFlockOptions}
|
isLoading={isLoadingFlockOptions}
|
||||||
isMulti={
|
isMulti={
|
||||||
@@ -450,6 +511,8 @@ const DashboardProduction = () => {
|
|||||||
formik.setFieldValue('kandang', selected)
|
formik.setFieldValue('kandang', selected)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.kandang as string}
|
errorMessage={formik.errors.kandang as string}
|
||||||
|
onInputChange={setInputValueKandang}
|
||||||
|
onMenuScrollToBottom={loadMoreKandang}
|
||||||
options={kandangOptions}
|
options={kandangOptions}
|
||||||
isLoading={isLoadingKandangOptions}
|
isLoading={isLoadingKandangOptions}
|
||||||
isMulti={
|
isMulti={
|
||||||
@@ -465,7 +528,9 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<div className='w-full p-4'>
|
||||||
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
|
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
|
||||||
@@ -473,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 || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,261 +295,382 @@ const DashboardLineChart = ({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chart */}
|
{/* Chart Container with Empty State Overlay */}
|
||||||
<ResponsiveContainer width='100%' height={350}>
|
<div className='relative'>
|
||||||
<LineChart
|
{/* Chart */}
|
||||||
data={(() => {
|
<ResponsiveContainer width='100%' height={350}>
|
||||||
// Transform data based on analysisMode
|
<LineChart
|
||||||
if (analysisMode === 'OVERVIEW') {
|
data={(() => {
|
||||||
// For OVERVIEW mode, use the selected chart data
|
// Transform data based on analysisMode
|
||||||
if (isOverviewCharts(data.charts)) {
|
if (analysisMode === 'OVERVIEW') {
|
||||||
const selectedChartData = data.charts[chartData];
|
// For OVERVIEW mode, use the selected chart data
|
||||||
if (!selectedChartData || !selectedChartData.dataset) return [];
|
if (isOverviewCharts(data.charts)) {
|
||||||
return selectedChartData.dataset;
|
const selectedChartData = data.charts[chartData];
|
||||||
|
if (!selectedChartData || !selectedChartData.dataset)
|
||||||
|
return [];
|
||||||
|
return selectedChartData.dataset;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
// For COMPARISON mode, use the first available comparison chart
|
||||||
|
if (isComparisonCharts(data.charts)) {
|
||||||
|
const chartData =
|
||||||
|
data.charts.farm ||
|
||||||
|
data.charts.flock ||
|
||||||
|
data.charts.kandang;
|
||||||
|
|
||||||
|
if (!chartData || !chartData.dataset) return [];
|
||||||
|
return chartData.dataset;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
} else {
|
|
||||||
// For COMPARISON mode, use the first available comparison chart
|
|
||||||
if (isComparisonCharts(data.charts)) {
|
|
||||||
const chartData =
|
|
||||||
data.charts.location ||
|
|
||||||
data.charts.flock ||
|
|
||||||
data.charts.kandang;
|
|
||||||
|
|
||||||
if (!chartData || !chartData.dataset) return [];
|
|
||||||
return chartData.dataset;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
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={(() => {
|
|
||||||
// Calculate dynamic domain based on visible data
|
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
|
||||||
let dataset: DashboardChartsDataset[] = [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
analysisMode === 'OVERVIEW' &&
|
|
||||||
isOverviewCharts(data.charts)
|
|
||||||
) {
|
|
||||||
seriesData = data.charts[chartData]?.series || [];
|
|
||||||
dataset = data.charts[chartData]?.dataset || [];
|
|
||||||
} else if (
|
|
||||||
analysisMode === 'COMPARISON' &&
|
|
||||||
isComparisonCharts(data.charts)
|
|
||||||
) {
|
|
||||||
const comparisonChart =
|
|
||||||
data.charts.location ||
|
|
||||||
data.charts.flock ||
|
|
||||||
data.charts.kandang;
|
|
||||||
seriesData = comparisonChart?.series || [];
|
|
||||||
dataset = comparisonChart?.dataset || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all values from visible series
|
|
||||||
const visibleSeriesIds = Array.from(visibleSeries);
|
|
||||||
const allValues: number[] = [];
|
|
||||||
|
|
||||||
dataset.forEach((item: DashboardChartsDataset) => {
|
|
||||||
visibleSeriesIds.forEach((seriesId) => {
|
|
||||||
const value = item[seriesId];
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
allValues.push(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allValues.length === 0) return [0, 100];
|
|
||||||
|
|
||||||
const minValue = Math.min(...allValues);
|
|
||||||
const maxValue = Math.max(...allValues);
|
|
||||||
|
|
||||||
// Add padding (10% on each side)
|
|
||||||
const padding = (maxValue - minValue) * 0.1;
|
|
||||||
const domainMin = Math.floor(Math.max(0, minValue - padding));
|
|
||||||
const domainMax = Math.ceil(maxValue + padding);
|
|
||||||
|
|
||||||
return [domainMin, domainMax];
|
|
||||||
})()}
|
})()}
|
||||||
ticks={(() => {
|
margin={{
|
||||||
// Calculate dynamic ticks based on domain
|
top: 5,
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
right: 10,
|
||||||
let dataset: DashboardChartsDataset[] = [];
|
left: 0,
|
||||||
|
bottom: 5,
|
||||||
if (
|
|
||||||
analysisMode === 'OVERVIEW' &&
|
|
||||||
isOverviewCharts(data.charts)
|
|
||||||
) {
|
|
||||||
seriesData = data.charts[chartData]?.series || [];
|
|
||||||
dataset = data.charts[chartData]?.dataset || [];
|
|
||||||
} else if (
|
|
||||||
analysisMode === 'COMPARISON' &&
|
|
||||||
isComparisonCharts(data.charts)
|
|
||||||
) {
|
|
||||||
const comparisonChart =
|
|
||||||
data.charts.location ||
|
|
||||||
data.charts.flock ||
|
|
||||||
data.charts.kandang;
|
|
||||||
seriesData = comparisonChart?.series || [];
|
|
||||||
dataset = comparisonChart?.dataset || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleSeriesIds = Array.from(visibleSeries);
|
|
||||||
const allValues: number[] = [];
|
|
||||||
|
|
||||||
dataset.forEach((item: DashboardChartsDataset) => {
|
|
||||||
visibleSeriesIds.forEach((seriesId) => {
|
|
||||||
const value = item[seriesId];
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
allValues.push(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allValues.length === 0) return [0, 25, 50, 75, 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);
|
|
||||||
|
|
||||||
// Generate 5 evenly spaced ticks
|
|
||||||
const range = domainMax - domainMin;
|
|
||||||
const step = range / 4;
|
|
||||||
|
|
||||||
return [
|
|
||||||
domainMin,
|
|
||||||
Math.round(domainMin + step),
|
|
||||||
Math.round(domainMin + step * 2),
|
|
||||||
Math.round(domainMin + step * 3),
|
|
||||||
domainMax,
|
|
||||||
];
|
|
||||||
})()}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: '#1f2937',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '8px 12px',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
}}
|
||||||
labelStyle={{ color: 'white', marginBottom: '4px' }}
|
>
|
||||||
itemStyle={{ color: 'white', fontSize: '12px' }}
|
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
||||||
labelFormatter={(value) => `Week ${value}`}
|
<XAxis
|
||||||
formatter={(
|
dataKey='week'
|
||||||
value: number | undefined,
|
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||||
name: string | undefined
|
tickLine={false}
|
||||||
) => {
|
axisLine={{ stroke: '#e5e7eb' }}
|
||||||
if (value === undefined || name === undefined) return ['', ''];
|
label={{
|
||||||
|
value: 'Weeks',
|
||||||
|
position: 'insideBottom',
|
||||||
|
offset: -5,
|
||||||
|
style: { fontSize: 12, fill: '#9ca3af' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={{ stroke: '#e5e7eb' }}
|
||||||
|
domain={(() => {
|
||||||
|
// Calculate dynamic domain based on visible data
|
||||||
|
let seriesData: DashboardChartsSeries[] = [];
|
||||||
|
let dataset: DashboardChartsDataset[] = [];
|
||||||
|
|
||||||
// Get series data to find the unit
|
if (
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
analysisMode === 'OVERVIEW' &&
|
||||||
if (
|
isOverviewCharts(data.charts)
|
||||||
analysisMode === 'OVERVIEW' &&
|
) {
|
||||||
isOverviewCharts(data.charts)
|
seriesData = data.charts[chartData]?.series || [];
|
||||||
) {
|
dataset = data.charts[chartData]?.dataset || [];
|
||||||
seriesData = data.charts[chartData]?.series || [];
|
} else if (
|
||||||
} else if (
|
analysisMode === 'COMPARISON' &&
|
||||||
analysisMode === 'COMPARISON' &&
|
isComparisonCharts(data.charts)
|
||||||
isComparisonCharts(data.charts)
|
) {
|
||||||
) {
|
const comparisonChart =
|
||||||
const comparisonChart =
|
data.charts.farm ||
|
||||||
data.charts.location ||
|
data.charts.flock ||
|
||||||
data.charts.flock ||
|
data.charts.kandang;
|
||||||
data.charts.kandang;
|
seriesData = comparisonChart?.series || [];
|
||||||
seriesData = comparisonChart?.series || [];
|
dataset = comparisonChart?.dataset || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the series that matches this line's name
|
// Get all values from visible series
|
||||||
const series = seriesData.find((s) => s.label === name);
|
const visibleSeriesIds = Array.from(visibleSeries);
|
||||||
const unit = series?.unit || '';
|
const allValues: number[] = [];
|
||||||
|
|
||||||
return [`${value} ${unit}`, name];
|
dataset.forEach((item: DashboardChartsDataset) => {
|
||||||
}}
|
visibleSeriesIds.forEach((seriesId) => {
|
||||||
/>
|
const value = item[seriesId];
|
||||||
{/* Dynamic Line rendering based on visible series */}
|
if (typeof value === 'number') {
|
||||||
{(() => {
|
allValues.push(value);
|
||||||
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.location ||
|
|
||||||
data.charts.flock ||
|
|
||||||
data.charts.kandang;
|
|
||||||
seriesData = comparisonChart?.series || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return seriesData
|
|
||||||
.filter((series) => visibleSeries.has(series.id))
|
|
||||||
.map((series, index) => {
|
|
||||||
const isStandard = series.id
|
|
||||||
.toString()
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('std');
|
|
||||||
// Use series.id directly as dataKey to match dataset fields
|
|
||||||
const dataKey = series.id.toString();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Line
|
|
||||||
key={series.id}
|
|
||||||
type='monotone'
|
|
||||||
dataKey={dataKey}
|
|
||||||
name={series.label}
|
|
||||||
stroke={getLineColor(series.id, index, analysisMode)}
|
|
||||||
opacity={isStandard ? 0.5 : 1}
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeDasharray={isStandard ? '5 5' : undefined}
|
|
||||||
dot={
|
|
||||||
isStandard
|
|
||||||
? false
|
|
||||||
: {
|
|
||||||
r: 3,
|
|
||||||
fill: '#fff',
|
|
||||||
stroke: getLineColor(
|
|
||||||
series.id,
|
|
||||||
index,
|
|
||||||
analysisMode
|
|
||||||
),
|
|
||||||
strokeWidth: 2,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
activeDot={isStandard ? undefined : { r: 5 }}
|
});
|
||||||
/>
|
});
|
||||||
|
|
||||||
|
if (allValues.length === 0) return [0, 100];
|
||||||
|
|
||||||
|
const minValue = Math.min(...allValues);
|
||||||
|
const maxValue = Math.max(...allValues);
|
||||||
|
|
||||||
|
// Add padding (10% on each side)
|
||||||
|
const padding = (maxValue - minValue) * 0.1;
|
||||||
|
const domainMin = Math.floor(Math.max(0, minValue - padding));
|
||||||
|
const domainMax = Math.ceil(maxValue + padding);
|
||||||
|
|
||||||
|
return [domainMin, domainMax];
|
||||||
|
})()}
|
||||||
|
ticks={(() => {
|
||||||
|
// Calculate dynamic ticks based on domain
|
||||||
|
let seriesData: DashboardChartsSeries[] = [];
|
||||||
|
let dataset: DashboardChartsDataset[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
analysisMode === 'OVERVIEW' &&
|
||||||
|
isOverviewCharts(data.charts)
|
||||||
|
) {
|
||||||
|
seriesData = data.charts[chartData]?.series || [];
|
||||||
|
dataset = data.charts[chartData]?.dataset || [];
|
||||||
|
} else if (
|
||||||
|
analysisMode === 'COMPARISON' &&
|
||||||
|
isComparisonCharts(data.charts)
|
||||||
|
) {
|
||||||
|
const comparisonChart =
|
||||||
|
data.charts.farm ||
|
||||||
|
data.charts.flock ||
|
||||||
|
data.charts.kandang;
|
||||||
|
seriesData = comparisonChart?.series || [];
|
||||||
|
dataset = comparisonChart?.dataset || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleSeriesIds = Array.from(visibleSeries);
|
||||||
|
const allValues: number[] = [];
|
||||||
|
|
||||||
|
dataset.forEach((item: DashboardChartsDataset) => {
|
||||||
|
visibleSeriesIds.forEach((seriesId) => {
|
||||||
|
const value = item[seriesId];
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
allValues.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allValues.length === 0) return [0, 25, 50, 75, 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);
|
||||||
|
|
||||||
|
// Generate 5 evenly spaced ticks
|
||||||
|
const range = domainMax - domainMin;
|
||||||
|
const step = range / 4;
|
||||||
|
|
||||||
|
return [
|
||||||
|
domainMin,
|
||||||
|
Math.round(domainMin + step),
|
||||||
|
Math.round(domainMin + step * 2),
|
||||||
|
Math.round(domainMin + step * 3),
|
||||||
|
domainMax,
|
||||||
|
];
|
||||||
|
})()}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#1f2937',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '8px 12px',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: 'white', marginBottom: '4px' }}
|
||||||
|
itemStyle={{ color: 'white', fontSize: '12px' }}
|
||||||
|
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={(
|
||||||
</LineChart>
|
value: number | undefined,
|
||||||
</ResponsiveContainer>
|
name: string | undefined
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
value === undefined ||
|
||||||
|
name === undefined ||
|
||||||
|
name.startsWith('STD. ')
|
||||||
|
)
|
||||||
|
return [undefined, undefined];
|
||||||
|
|
||||||
|
// 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 === name);
|
||||||
|
const id = series?.id || '';
|
||||||
|
|
||||||
|
return [value, id];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Dynamic Line rendering based on visible series */}
|
||||||
|
{(() => {
|
||||||
|
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 || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return seriesData
|
||||||
|
.filter((series) => visibleSeries.has(series.id))
|
||||||
|
.map((series, index) => {
|
||||||
|
const isStandard = series.id
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('std');
|
||||||
|
// Use series.id directly as dataKey to match dataset fields
|
||||||
|
const dataKey = series.id.toString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={series.id}
|
||||||
|
type='monotone'
|
||||||
|
dataKey={dataKey}
|
||||||
|
name={series.label}
|
||||||
|
stroke={getLineColor(series.id, index, analysisMode)}
|
||||||
|
opacity={isStandard ? 0.5 : 1}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray={isStandard ? '5 5' : undefined}
|
||||||
|
dot={
|
||||||
|
isStandard
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
r: 3,
|
||||||
|
fill: '#fff',
|
||||||
|
stroke: getLineColor(
|
||||||
|
series.id,
|
||||||
|
index,
|
||||||
|
analysisMode
|
||||||
|
),
|
||||||
|
strokeWidth: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeDot={isStandard ? undefined : { r: 5 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
|
||||||
|
{/* Empty State Overlay */}
|
||||||
|
{(() => {
|
||||||
|
// Get current dataset
|
||||||
|
let dataset: DashboardChartsDataset[] = [];
|
||||||
|
|
||||||
|
if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) {
|
||||||
|
dataset = data.charts[chartData]?.dataset || [];
|
||||||
|
} else if (
|
||||||
|
analysisMode === 'COMPARISON' &&
|
||||||
|
isComparisonCharts(data.charts)
|
||||||
|
) {
|
||||||
|
const comparisonChart =
|
||||||
|
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||||
|
dataset = comparisonChart?.dataset || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show empty state if dataset is empty
|
||||||
|
if (dataset.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='absolute inset-x-0 inset-y-15 z-10 flex flex-col items-center justify-center rounded-lg'>
|
||||||
|
{/* Chart icon */}
|
||||||
|
<div className='w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mb-4'>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chart-bar'
|
||||||
|
className='text-white'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Empty state text */}
|
||||||
|
<h3 className='text-gray-900 font-semibold text-base mb-2'>
|
||||||
|
Data Not Yet Available
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-500 text-sm text-center max-w-xs'>
|
||||||
|
Please change your filters to get the data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import { CellContext, Row } from '@tanstack/react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
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/Dropdown';
|
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import SelectInput, {
|
import SelectInput, {
|
||||||
OptionType,
|
OptionType,
|
||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import Menu from '@/components/menu/Menu';
|
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Tooltip from '@/components/Tooltip';
|
|
||||||
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { Finance } from '@/types/api/finance/finance';
|
import { Finance } from '@/types/api/finance/finance';
|
||||||
@@ -23,7 +19,6 @@ import {
|
|||||||
FINANCE_INITIAL_BALANCE_STATUS,
|
FINANCE_INITIAL_BALANCE_STATUS,
|
||||||
FINANCE_INJECTION_STATUS,
|
FINANCE_INJECTION_STATUS,
|
||||||
FINANCE_TRANSACTION_STATUS,
|
FINANCE_TRANSACTION_STATUS,
|
||||||
ROWS_OPTIONS,
|
|
||||||
} from '@/config/constant';
|
} from '@/config/constant';
|
||||||
import { FinanceApi } from '@/services/api/finance';
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -70,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
|
||||||
@@ -199,35 +199,37 @@ 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' },
|
||||||
{ label: 'Tanggal Dibuat', value: 'created_at' },
|
{ label: 'Tanggal Dibuat', value: 'created_at' },
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
const { options: bankOptions, rawData: bankRawData } = useSelect<Bank>(
|
const {
|
||||||
BankApi.basePath,
|
options: bankOptions,
|
||||||
'id',
|
rawData: bankRawData,
|
||||||
'alias',
|
setInputValue: bankInputValue,
|
||||||
'',
|
loadMore: bankLoadMore,
|
||||||
{
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'alias');
|
||||||
limit: 'limit',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== Handler =====
|
// ===== Handler =====
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
@@ -344,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>;
|
||||||
},
|
},
|
||||||
@@ -368,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)',
|
||||||
@@ -476,38 +478,49 @@ 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,
|
||||||
}))
|
}))
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
label='Bank'
|
label='Bank'
|
||||||
value={selectedBank}
|
value={selectedBank}
|
||||||
onChange={bankChangeHandler}
|
onChange={bankChangeHandler}
|
||||||
isClearable
|
onInputChange={bankInputValue}
|
||||||
/>
|
onMenuScrollToBottom={bankLoadMore}
|
||||||
<SelectInput
|
|
||||||
options={partyTypeOptions}
|
|
||||||
label='Pihak'
|
|
||||||
value={selectedPartyType}
|
|
||||||
onChange={partyTypeChangeHandler}
|
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -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 || '',
|
||||||
@@ -113,20 +119,22 @@ const FormFinanceAdd = ({
|
|||||||
options: partyOptions,
|
options: partyOptions,
|
||||||
isLoadingOptions: isLoadingPartyOptions,
|
isLoadingOptions: isLoadingPartyOptions,
|
||||||
rawData: partyRawData,
|
rawData: partyRawData,
|
||||||
|
setInputValue: setPartyInputValue,
|
||||||
|
loadMore: loadMorePartyOptions,
|
||||||
} = useSelect<PartyCommonProps>(
|
} = useSelect<PartyCommonProps>(
|
||||||
formik.values.party_type_option?.value === 'CUSTOMER'
|
formik.values.party_type_option?.value === 'CUSTOMER'
|
||||||
? CustomerApi.basePath
|
? CustomerApi.basePath
|
||||||
: SupplierApi.basePath,
|
: SupplierApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'name'
|
||||||
'',
|
|
||||||
{ limit: 'limit' }
|
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
options: bankOptions,
|
options: bankOptions,
|
||||||
rawData: bankRawData,
|
rawData: bankRawData,
|
||||||
isLoadingOptions: isLoadingBankOptions,
|
isLoadingOptions: isLoadingBankOptions,
|
||||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
setInputValue: setBankInputValue,
|
||||||
|
loadMore: loadMoreBankOptions,
|
||||||
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// ===== Helper Functions =====
|
// ===== Helper Functions =====
|
||||||
const transformFormValuesToPayload = (
|
const transformFormValuesToPayload = (
|
||||||
@@ -151,6 +159,7 @@ const FormFinanceAdd = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +175,7 @@ const FormFinanceAdd = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +215,7 @@ const FormFinanceAdd = ({
|
|||||||
? formik.errors.party_type_option
|
? formik.errors.party_type_option
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
isDisabled={type === 'edit' || isSupplier}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
@@ -219,6 +230,8 @@ const FormFinanceAdd = ({
|
|||||||
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`}
|
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`}
|
||||||
options={partyOptions}
|
options={partyOptions}
|
||||||
value={formik.values.party_id_option}
|
value={formik.values.party_id_option}
|
||||||
|
onInputChange={setPartyInputValue}
|
||||||
|
onMenuScrollToBottom={loadMorePartyOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('party_id_option', value);
|
formik.setFieldValue('party_id_option', value);
|
||||||
if (isResponseSuccess(partyRawData) && value) {
|
if (isResponseSuccess(partyRawData) && value) {
|
||||||
@@ -241,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'
|
||||||
@@ -259,6 +272,7 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Metode Pembayaran'
|
label='Metode Pembayaran'
|
||||||
@@ -280,6 +294,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
|
isDisabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Bank'
|
label='Bank'
|
||||||
@@ -304,6 +319,8 @@ const FormFinanceAdd = ({
|
|||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
value={formik.values.bank_id_option}
|
value={formik.values.bank_id_option}
|
||||||
|
onInputChange={setBankInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreBankOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('bank_id_option', value);
|
formik.setFieldValue('bank_id_option', value);
|
||||||
}}
|
}}
|
||||||
@@ -318,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'}`}
|
||||||
@@ -338,6 +356,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
readOnly
|
readOnly
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label='Nomor Referensi'
|
label='Nomor Referensi'
|
||||||
@@ -357,6 +376,7 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label='Nominal'
|
label='Nominal'
|
||||||
@@ -372,6 +392,7 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
label='Catatan'
|
label='Catatan'
|
||||||
@@ -387,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 || '',
|
||||||
@@ -104,21 +106,25 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===== Options =====
|
// ===== Options =====
|
||||||
const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } =
|
const {
|
||||||
useSelect(
|
options: partyOptions,
|
||||||
formik.values.party_type_option?.value === 'CUSTOMER'
|
isLoadingOptions: isLoadingPartyOptions,
|
||||||
? CustomerApi.basePath
|
setInputValue: setPartyInputValue,
|
||||||
: SupplierApi.basePath,
|
loadMore: loadMorePartyOptions,
|
||||||
'id',
|
} = useSelect(
|
||||||
'name',
|
formik.values.party_type_option?.value === 'CUSTOMER'
|
||||||
'',
|
? CustomerApi.basePath
|
||||||
{ limit: 'limit' }
|
: SupplierApi.basePath,
|
||||||
);
|
'id',
|
||||||
|
'name'
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
options: bankOptions,
|
options: bankOptions,
|
||||||
rawData: bankRawData,
|
rawData: bankRawData,
|
||||||
isLoadingOptions: isLoadingBankOptions,
|
isLoadingOptions: isLoadingBankOptions,
|
||||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
setInputValue: setBankInputValue,
|
||||||
|
loadMore: loadMoreBankOptions,
|
||||||
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// ===== Helper Functions =====
|
// ===== Helper Functions =====
|
||||||
const transformFormValuesToPayload = (
|
const transformFormValuesToPayload = (
|
||||||
@@ -143,6 +149,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +169,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +197,8 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
placeholder='Pilih jenis pihak'
|
placeholder='Pilih jenis pihak'
|
||||||
options={FINANCE_PARTY_TYPE_OPTIONS}
|
options={FINANCE_PARTY_TYPE_OPTIONS}
|
||||||
value={formik.values.party_type_option}
|
value={formik.values.party_type_option}
|
||||||
|
onInputChange={setPartyInputValue}
|
||||||
|
onMenuScrollToBottom={loadMorePartyOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('party_type_option', value);
|
formik.setFieldValue('party_type_option', value);
|
||||||
formik.setFieldValue('party_id_option', null);
|
formik.setFieldValue('party_id_option', null);
|
||||||
@@ -205,6 +215,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
isDisabled={type === 'edit'}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -218,6 +229,8 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis pihak dahulu'}`}
|
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis pihak dahulu'}`}
|
||||||
options={partyOptions}
|
options={partyOptions}
|
||||||
value={formik.values.party_id_option}
|
value={formik.values.party_id_option}
|
||||||
|
onInputChange={setPartyInputValue}
|
||||||
|
onMenuScrollToBottom={loadMorePartyOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('party_id_option', value);
|
formik.setFieldValue('party_id_option', value);
|
||||||
}}
|
}}
|
||||||
@@ -269,7 +282,6 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
? formik.errors.bank_id_option
|
? formik.errors.bank_id_option
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -354,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 || '',
|
||||||
@@ -80,7 +83,9 @@ const FormFinanceInjection = ({
|
|||||||
options: bankOptions,
|
options: bankOptions,
|
||||||
rawData: bankRawData,
|
rawData: bankRawData,
|
||||||
isLoadingOptions: isLoadingBankOptions,
|
isLoadingOptions: isLoadingBankOptions,
|
||||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
setInputValue: setBankInputValue,
|
||||||
|
loadMore: loadMoreBankOptions,
|
||||||
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// ===== Helper Functions =====
|
// ===== Helper Functions =====
|
||||||
const transformFormValuesToPayload = (
|
const transformFormValuesToPayload = (
|
||||||
@@ -101,6 +106,7 @@ const FormFinanceInjection = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +123,7 @@ const FormFinanceInjection = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +169,8 @@ const FormFinanceInjection = ({
|
|||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
value={formik.values.bank_id_option}
|
value={formik.values.bank_id_option}
|
||||||
|
onInputChange={setBankInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreBankOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('bank_id_option', value);
|
formik.setFieldValue('bank_id_option', value);
|
||||||
}}
|
}}
|
||||||
@@ -226,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'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||||
import {
|
import {
|
||||||
CreateInventoryAdjustmentPayload,
|
CreateInventoryAdjustmentPayload,
|
||||||
@@ -22,12 +22,18 @@ import {
|
|||||||
} from '@/services/api/master-data';
|
} from '@/services/api/master-data';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import { RadioGroup } from '@/components/input/RadioInput';
|
import { RadioGroup } from '@/components/input/RadioInput';
|
||||||
import TextArea from '@/components/input/TextArea';
|
import TextArea from '@/components/input/TextArea';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
|
import { Product } from '@/types/api/master-data/product';
|
||||||
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
|
|
||||||
interface InventoryAdjustmentFormProps {
|
interface InventoryAdjustmentFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -44,10 +50,7 @@ const InventoryAdjustmentForm = ({
|
|||||||
InventoryAdjustmentFormErrorMessage,
|
InventoryAdjustmentFormErrorMessage,
|
||||||
setInventoryAdjustmentFormErrorMessage,
|
setInventoryAdjustmentFormErrorMessage,
|
||||||
] = useState('');
|
] = useState('');
|
||||||
const [selectedProductCategories, setSelectedProductCategories] =
|
|
||||||
useState('');
|
|
||||||
const [disabledProduct, setDisabledProduct] = useState(true);
|
const [disabledProduct, setDisabledProduct] = useState(true);
|
||||||
const [optionsProduct, setOptionsProduct] = useState<OptionType[]>([]);
|
|
||||||
const [quantityLabel, setQuantityLabel] = useState('Tambah Stok');
|
const [quantityLabel, setQuantityLabel] = useState('Tambah Stok');
|
||||||
|
|
||||||
// Submit Handler
|
// Submit Handler
|
||||||
@@ -108,45 +111,30 @@ const InventoryAdjustmentForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fetch Data
|
// Fetch Data
|
||||||
const productCategoriesUrl = `${
|
const {
|
||||||
ProductCategoryApi.basePath
|
setInputValue: setProductCategoryInputValue,
|
||||||
}?${new URLSearchParams({
|
options: productCategoryOptions,
|
||||||
search: '',
|
isLoadingOptions: isLoadingProductCategoryOptions,
|
||||||
}).toString()}`;
|
loadMore: loadMoreProductCategories,
|
||||||
const { data: productCategories, isLoading: isLoadingProductCategories } =
|
} = useSelect<ProductCategory>(ProductCategoryApi.basePath, 'id', 'name');
|
||||||
useSWR(productCategoriesUrl, ProductCategoryApi.getAllFetcher);
|
|
||||||
|
|
||||||
const productUrl = `${ProductApi.basePath}?${new URLSearchParams({
|
const {
|
||||||
search: '',
|
setInputValue: setProductInputValue,
|
||||||
product_category_id: selectedProductCategories,
|
options: productOptions,
|
||||||
}).toString()}`;
|
isLoadingOptions: isLoadingProductOptions,
|
||||||
const { data: products, isLoading: isLoadingProducts } = useSWR(
|
loadMore: loadMoreProducts,
|
||||||
productUrl,
|
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
|
||||||
ProductApi.getAllFetcher
|
product_category_id: formik.values.product_category_id
|
||||||
);
|
? String(formik.values.product_category_id)
|
||||||
|
: '',
|
||||||
|
});
|
||||||
|
|
||||||
const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({
|
const {
|
||||||
search: '',
|
setInputValue: setWarehouseInputValue,
|
||||||
limit: '100',
|
options: warehouseOptions,
|
||||||
}).toString()}`;
|
isLoadingOptions: isLoadingWarehouseOptions,
|
||||||
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
|
loadMore: loadMoreWarehouses,
|
||||||
warehouseUrl,
|
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
||||||
WarehouseApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
// Map Data to Options
|
|
||||||
const optionsProductCategory = isResponseSuccess(productCategories)
|
|
||||||
? productCategories?.data.map((productCategory) => ({
|
|
||||||
value: productCategory.id,
|
|
||||||
label: productCategory.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
const optionsWarehouse = isResponseSuccess(warehouses)
|
|
||||||
? warehouses?.data.map((warehouse) => ({
|
|
||||||
value: warehouse.id,
|
|
||||||
label: warehouse.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Options Handler
|
// Options Handler
|
||||||
const productCategoryChangeHandler = (
|
const productCategoryChangeHandler = (
|
||||||
@@ -157,7 +145,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
|
|
||||||
formik.setFieldValue('product_category', val);
|
formik.setFieldValue('product_category', val);
|
||||||
|
|
||||||
setSelectedProductCategories((val as OptionType)?.value as string);
|
|
||||||
const disabled = (val as OptionType)?.value == null;
|
const disabled = (val as OptionType)?.value == null;
|
||||||
setDisabledProduct(disabled);
|
setDisabledProduct(disabled);
|
||||||
formik.setFieldValue('product_id', 0);
|
formik.setFieldValue('product_id', 0);
|
||||||
@@ -193,9 +180,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
// Effect
|
// Effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues?.product_warehouse?.product?.id) {
|
if (initialValues?.product_warehouse?.product?.id) {
|
||||||
setSelectedProductCategories(
|
|
||||||
String(initialValues.product_warehouse.product.id)
|
|
||||||
);
|
|
||||||
setDisabledProduct(false);
|
setDisabledProduct(false);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'product_id',
|
'product_id',
|
||||||
@@ -219,25 +203,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
);
|
);
|
||||||
formik.setFieldValue('note', initialValues.note);
|
formik.setFieldValue('note', initialValues.note);
|
||||||
}
|
}
|
||||||
}, [
|
}, [formik, initialValues, setQuantityLabel, setDisabledProduct]);
|
||||||
formik,
|
|
||||||
initialValues,
|
|
||||||
setQuantityLabel,
|
|
||||||
setDisabledProduct,
|
|
||||||
setSelectedProductCategories,
|
|
||||||
]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
|
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
|
||||||
}, [formikSetValues, formikInitialValues]);
|
}, [formikSetValues, formikInitialValues]);
|
||||||
useEffect(() => {
|
|
||||||
if (isResponseSuccess(products)) {
|
|
||||||
const options = products.data.map((p) => ({
|
|
||||||
value: p.id,
|
|
||||||
label: p.name,
|
|
||||||
}));
|
|
||||||
setOptionsProduct(options);
|
|
||||||
}
|
|
||||||
}, [products]);
|
|
||||||
|
|
||||||
// Utils Function
|
// Utils Function
|
||||||
const formatNumber = (value: string) => {
|
const formatNumber = (value: string) => {
|
||||||
@@ -282,9 +251,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
label='Kategori Produk'
|
label='Kategori Produk'
|
||||||
value={formik.values.product_category as OptionType}
|
value={formik.values.product_category as OptionType}
|
||||||
onChange={productCategoryChangeHandler}
|
onChange={productCategoryChangeHandler}
|
||||||
onInputChange={setSelectedProductCategories}
|
onInputChange={setProductCategoryInputValue}
|
||||||
options={optionsProductCategory}
|
options={productCategoryOptions}
|
||||||
isLoading={isLoadingProductCategories}
|
onMenuScrollToBottom={loadMoreProductCategories}
|
||||||
|
isLoading={isLoadingProductCategoryOptions}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.product_category &&
|
formik.touched.product_category &&
|
||||||
Boolean(formik.errors.product_category)
|
Boolean(formik.errors.product_category)
|
||||||
@@ -300,8 +270,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
value={formik.values.product as OptionType}
|
value={formik.values.product as OptionType}
|
||||||
onChange={productChangeHandler}
|
onChange={productChangeHandler}
|
||||||
options={optionsProduct}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProducts}
|
options={productOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreProducts}
|
||||||
|
isLoading={isLoadingProductOptions}
|
||||||
isError={formik.touched.product && Boolean(formik.errors.product)}
|
isError={formik.touched.product && Boolean(formik.errors.product)}
|
||||||
errorMessage={formik.errors.product as string}
|
errorMessage={formik.errors.product as string}
|
||||||
isDisabled={type === 'detail' || disabledProduct}
|
isDisabled={type === 'detail' || disabledProduct}
|
||||||
@@ -314,8 +286,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
label='Warehouse'
|
label='Warehouse'
|
||||||
value={formik.values.warehouse as OptionType}
|
value={formik.values.warehouse as OptionType}
|
||||||
onChange={warehouseChangeHandler}
|
onChange={warehouseChangeHandler}
|
||||||
options={optionsWarehouse}
|
onInputChange={setWarehouseInputValue}
|
||||||
isLoading={isLoadingWarehouses}
|
options={warehouseOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
|
isLoading={isLoadingWarehouseOptions}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.warehouse && Boolean(formik.errors.warehouse)
|
formik.touched.warehouse && Boolean(formik.errors.warehouse)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
@@ -38,6 +38,8 @@ import Card from '@/components/Card';
|
|||||||
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
|
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
import { getUniqueFormikErrors } from '@/lib/formik-helper';
|
import { getUniqueFormikErrors } from '@/lib/formik-helper';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
|
|
||||||
interface MovementFormProps {
|
interface MovementFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -49,10 +51,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
|
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
|
||||||
const [
|
|
||||||
productWarehouseSelectInputValue,
|
|
||||||
setProductWarehouseSelectInputValue,
|
|
||||||
] = useState('');
|
|
||||||
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
||||||
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
|
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
|
||||||
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
||||||
@@ -93,10 +91,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
// ===== USE SELECT HOOKS =====
|
// ===== USE SELECT HOOKS =====
|
||||||
const {
|
const {
|
||||||
inputValue: warehouseSelectInputValue,
|
|
||||||
setInputValue: setWarehouseSelectInputValue,
|
setInputValue: setWarehouseSelectInputValue,
|
||||||
isLoadingOptions: isLoadingWarehouses,
|
isLoadingOptions: isLoadingWarehouses,
|
||||||
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
|
loadMore: loadMoreWarehouses,
|
||||||
|
rawData: warehouses,
|
||||||
|
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search', {
|
||||||
|
flag: 'EKSPEDISI',
|
||||||
|
});
|
||||||
|
|
||||||
// ===== SELECT INPUT DATA =====
|
// ===== SELECT INPUT DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -107,12 +108,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
category: 'BOP',
|
category: 'BOP',
|
||||||
});
|
});
|
||||||
|
|
||||||
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
|
||||||
const { data: warehouses } = useSWR(
|
|
||||||
warehousesUrl,
|
|
||||||
WarehouseApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== DATA PROCESSING =====
|
// ===== DATA PROCESSING =====
|
||||||
const warehouseStockMap = useMemo(() => {
|
const warehouseStockMap = useMemo(() => {
|
||||||
if (!isResponseSuccess(allProductWarehouses)) return new Map();
|
if (!isResponseSuccess(allProductWarehouses)) return new Map();
|
||||||
@@ -268,26 +263,64 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
const prevSourceWarehouseIdRef = useRef<number | null>(
|
||||||
const getProductWarehousesUrl = useCallback(() => {
|
formik.values.source_warehouse_id
|
||||||
const productWarehouseParams = new URLSearchParams({
|
);
|
||||||
search: productWarehouseSelectInputValue,
|
|
||||||
});
|
|
||||||
if (formik.values.source_warehouse_id) {
|
|
||||||
productWarehouseParams.append(
|
|
||||||
'warehouse_id',
|
|
||||||
formik.values.source_warehouse_id.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
|
|
||||||
}, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]);
|
|
||||||
|
|
||||||
const productWarehousesUrl = getProductWarehousesUrl();
|
// ===== RESET PRODUCTS WHEN SOURCE WAREHOUSE CHANGES =====
|
||||||
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
|
useEffect(() => {
|
||||||
useSWR(
|
const prevSourceWarehouseId = prevSourceWarehouseIdRef.current;
|
||||||
formik.values.source_warehouse_id ? productWarehousesUrl : null,
|
const currentSourceWarehouseId = formik.values.source_warehouse_id;
|
||||||
ProductWarehouseApi.getAllFetcher
|
|
||||||
);
|
if (
|
||||||
|
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
||||||
|
prevSourceWarehouseId !== null
|
||||||
|
) {
|
||||||
|
formik.setFieldValue('products', [
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
formik.setFieldTouched('products', false);
|
||||||
|
|
||||||
|
const updatedDeliveries = formik.values.deliveries.map(
|
||||||
|
(delivery: DeliverySchema) => ({
|
||||||
|
...delivery,
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
|
formik.setFieldTouched('deliveries', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
||||||
|
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
|
||||||
|
|
||||||
|
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||||
|
const {
|
||||||
|
setInputValue: setProductWarehouseSelectInputValue,
|
||||||
|
isLoadingOptions: isLoadingProductWarehouses,
|
||||||
|
loadMore: loadMoreProductWarehouses,
|
||||||
|
rawData: productWarehouses,
|
||||||
|
} = useSelect<ProductWarehouse>(
|
||||||
|
formik.values.source_warehouse_id ? ProductWarehouseApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
warehouse_id: formik.values.source_warehouse_id
|
||||||
|
? formik.values.source_warehouse_id.toString()
|
||||||
|
: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
||||||
? productWarehouses?.data.map((pw) => ({
|
? productWarehouses?.data.map((pw) => ({
|
||||||
@@ -357,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 || []),
|
||||||
{
|
{
|
||||||
@@ -373,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 =
|
||||||
@@ -397,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 || []),
|
||||||
{
|
{
|
||||||
@@ -420,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 =
|
||||||
@@ -447,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);
|
||||||
@@ -492,7 +677,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostChangeWrapper = useCallback(
|
const handleDeliveryCostChangeWrapper = useCallback(
|
||||||
@@ -967,45 +1152,10 @@ 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}
|
||||||
isLoading={isLoadingWarehouses}
|
isLoading={isLoadingWarehouses}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.source_warehouse_id &&
|
formik.touched.source_warehouse_id &&
|
||||||
@@ -1066,44 +1216,11 @@ 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}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.destination_warehouse_id &&
|
formik.touched.destination_warehouse_id &&
|
||||||
Boolean(formik.errors.destination_warehouse_id)
|
Boolean(formik.errors.destination_warehouse_id)
|
||||||
@@ -1173,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',
|
||||||
@@ -1221,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',
|
||||||
@@ -1243,26 +1339,10 @@ 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}
|
||||||
isLoading={isLoadingProductWarehouses}
|
isLoading={isLoadingProductWarehouses}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
type === 'detail' ||
|
type === 'detail' ||
|
||||||
@@ -1386,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',
|
||||||
@@ -1481,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',
|
||||||
@@ -1507,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
|
||||||
@@ -1575,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}
|
||||||
@@ -1684,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',
|
||||||
@@ -184,12 +185,16 @@ const MarketingTable = () => {
|
|||||||
const {
|
const {
|
||||||
options: productsOptions,
|
options: productsOptions,
|
||||||
isLoadingOptions: isLoadingProductsOptions,
|
isLoadingOptions: isLoadingProductsOptions,
|
||||||
|
setInputValue: setProductsInputValue,
|
||||||
|
loadMore: loadMoreProducts,
|
||||||
} = useSelect(ProductApi.basePath, 'id', 'name', '', {
|
} = useSelect(ProductApi.basePath, 'id', 'name', '', {
|
||||||
limit: 'limit',
|
limit: 'limit',
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
options: customersOptions,
|
options: customersOptions,
|
||||||
isLoadingOptions: isLoadingCustomersOptions,
|
isLoadingOptions: isLoadingCustomersOptions,
|
||||||
|
setInputValue: setCustomersInputValue,
|
||||||
|
loadMore: loadMoreCustomers,
|
||||||
} = useSelect(CustomerApi.basePath, 'id', 'name', '', {
|
} = useSelect(CustomerApi.basePath, 'id', 'name', '', {
|
||||||
limit: 'limit',
|
limit: 'limit',
|
||||||
});
|
});
|
||||||
@@ -400,6 +405,8 @@ const MarketingTable = () => {
|
|||||||
.join(',') || ''
|
.join(',') || ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onInputChange={setProductsInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProducts}
|
||||||
isMulti
|
isMulti
|
||||||
/>
|
/>
|
||||||
{/* select status */}
|
{/* select status */}
|
||||||
@@ -444,6 +451,8 @@ const MarketingTable = () => {
|
|||||||
(value as OptionType)?.value.toString() || ''
|
(value as OptionType)?.value.toString() || ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onInputChange={setCustomersInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreCustomers}
|
||||||
/>
|
/>
|
||||||
</TableRowSizeSelector>
|
</TableRowSizeSelector>
|
||||||
</div>
|
</div>
|
||||||
@@ -512,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>
|
||||||
|
|||||||
@@ -633,7 +633,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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Area } from '@/types/api/master-data/area';
|
import { Area } from '@/types/api/master-data/area';
|
||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -164,7 +164,14 @@ const AreasTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await AreaApi.delete(selectedArea?.id as number);
|
const deleteResponse = await AreaApi.delete(selectedArea?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshAreas();
|
refreshAreas();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import { BankApi } from '@/services/api/master-data';
|
import { BankApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -177,7 +177,14 @@ const BanksTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await BankApi.delete(selectedBank?.id as number);
|
const deleteResponse = await BankApi.delete(selectedBank?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshBanks();
|
refreshBanks();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -186,7 +186,16 @@ const CustomersTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await CustomerApi.delete(selectedCustomer?.id as number);
|
const deleteResponse = await CustomerApi.delete(
|
||||||
|
selectedCustomer?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshCustomers();
|
refreshCustomers();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Fcr } from '@/types/api/master-data/fcr';
|
import { Fcr } from '@/types/api/master-data/fcr';
|
||||||
import { FcrApi } from '@/services/api/master-data';
|
import { FcrApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -164,7 +164,14 @@ const FcrsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await FcrApi.delete(selectedFcr?.id as number);
|
const deleteResponse = await FcrApi.delete(selectedFcr?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshFcrs();
|
refreshFcrs();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
|||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
|
||||||
const RowsOptions = ({
|
const RowsOptions = ({
|
||||||
@@ -33,22 +33,6 @@ const RowsOptions = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
<RequirePermission permissions='lti.master.flocks.update'>
|
|
||||||
<Button
|
|
||||||
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
|
|
||||||
variant='ghost'
|
|
||||||
color='warning'
|
|
||||||
className='justify-start text-sm'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:edit-outline'
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className='justify-start text-sm'
|
|
||||||
/>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
<RequirePermission permissions='lti.master.flocks.detail'>
|
<RequirePermission permissions='lti.master.flocks.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
|
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
|
||||||
@@ -65,6 +49,22 @@ const RowsOptions = ({
|
|||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.flocks.update'>
|
||||||
|
<Button
|
||||||
|
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='warning'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:edit-outline'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.master.flocks.delete'>
|
<RequirePermission permissions='lti.master.flocks.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
@@ -182,7 +182,14 @@ const FlockTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await FlockApi.delete(selectedFlock?.id as number);
|
const deleteResponse = await FlockApi.delete(selectedFlock?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshFlocks();
|
refreshFlocks();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
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 { toast } from 'react-hot-toast';
|
||||||
|
import Alert from '@/components/Alert';
|
||||||
|
|
||||||
interface FlockCustomProps {
|
interface FlockCustomProps {
|
||||||
formType?: 'add' | 'edit' | 'detail';
|
formType?: 'add' | 'edit' | 'detail';
|
||||||
@@ -37,7 +39,13 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await FlockApi.delete(initialValues?.id as number);
|
const deleteFlockRes = await FlockApi.delete(initialValues?.id as number);
|
||||||
|
if (deleteFlockRes?.status === 'error') {
|
||||||
|
setFlockFormErrorMessage(deleteFlockRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(deleteFlockRes?.message as string);
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
@@ -68,12 +76,29 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
|
|
||||||
// cek type form yang disubmit
|
// cek type form yang disubmit
|
||||||
switch (formType) {
|
switch (formType) {
|
||||||
case 'add':
|
case 'add': {
|
||||||
await FlockApi.create(payload);
|
const createFlockRes = await FlockApi.create(payload);
|
||||||
|
if (createFlockRes?.status === 'error') {
|
||||||
|
setFlockFormErrorMessage(createFlockRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(createFlockRes?.message as string);
|
||||||
break;
|
break;
|
||||||
case 'edit':
|
}
|
||||||
await FlockApi.update(initialValues?.id as number, payload);
|
case 'edit': {
|
||||||
|
const updateFlockRes = await FlockApi.update(
|
||||||
|
initialValues?.id as number,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
if (updateFlockRes?.status === 'error') {
|
||||||
|
setFlockFormErrorMessage(updateFlockRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(updateFlockRes?.message as string);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -174,6 +199,24 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
{flockFormErrorMessage && (
|
||||||
|
<Alert color='error' className='w-full'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
{flockFormErrorMessage}
|
||||||
|
<Button
|
||||||
|
onClick={() => setFlockFormErrorMessage('')}
|
||||||
|
variant='link'
|
||||||
|
className='ml-auto p-0 w-fit text-white'
|
||||||
|
color='none'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{formType !== 'detail' && (
|
{formType !== 'detail' && (
|
||||||
<div
|
<div
|
||||||
@@ -197,17 +240,6 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{flockFormErrorMessage && (
|
|
||||||
<div role='alert' className='alert alert-error'>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:error-outline'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
<span>{flockFormErrorMessage}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
import { cn, formatNumber } from '@/lib/helper';
|
import { cn, formatNumber } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -199,7 +199,16 @@ const KandangsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await KandangApi.delete(selectedKandang?.id as number);
|
const deleteResponse = await KandangApi.delete(
|
||||||
|
selectedKandang?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshKandangs();
|
refreshKandangs();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama lokasi'
|
placeholder='Masukkan nama kandang'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -186,7 +186,16 @@ const LocationsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await LocationApi.delete(selectedLocation?.id as number);
|
const deleteResponse = await LocationApi.delete(
|
||||||
|
selectedLocation?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshLocations();
|
refreshLocations();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
import { NonstockApi } from '@/services/api/master-data';
|
import { NonstockApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -198,7 +198,16 @@ const NonstocksTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await NonstockApi.delete(selectedNonstock?.id as number);
|
const deleteResponse = await NonstockApi.delete(
|
||||||
|
selectedNonstock?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshNonstocks();
|
refreshNonstocks();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
|||||||
const formikInitialValues = useMemo<NonstockFormValues>(() => {
|
const formikInitialValues = useMemo<NonstockFormValues>(() => {
|
||||||
return {
|
return {
|
||||||
name: initialValues?.name ?? '',
|
name: initialValues?.name ?? '',
|
||||||
uomId: initialValues?.uom_id ?? 0,
|
uomId: initialValues?.uom?.id ?? 0,
|
||||||
uom: initialValues?.uom
|
uom: initialValues?.uom
|
||||||
? {
|
? {
|
||||||
value: initialValues?.uom?.id,
|
value: initialValues?.uom?.id,
|
||||||
@@ -229,7 +229,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama lokasi'
|
placeholder='Masukkan nama nonstock'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -170,7 +170,16 @@ const ProductCategoryTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await ProductCategoryApi.delete(selectedProductCategory?.id as number);
|
const deleteResponse = await ProductCategoryApi.delete(
|
||||||
|
selectedProductCategory?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshProductCategories();
|
refreshProductCategories();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Product } from '@/types/api/master-data/product';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { ProductApi } from '@/services/api/master-data';
|
import { ProductApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -230,8 +230,19 @@ const ProductsTable = () => {
|
|||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
await ProductApi.delete(selectedProduct?.id as number);
|
|
||||||
|
const deleteResponse = await ProductApi.delete(
|
||||||
|
selectedProduct?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshProducts();
|
refreshProducts();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
toast.success('Successfully delete Product!');
|
toast.success('Successfully delete Product!');
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as Yup from 'yup';
|
|||||||
type ProductFormSchemaType = {
|
type ProductFormSchemaType = {
|
||||||
name: string;
|
name: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
sku: string;
|
sku?: string;
|
||||||
uom?: {
|
uom?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -15,10 +15,16 @@ type ProductFormSchemaType = {
|
|||||||
} | null;
|
} | null;
|
||||||
product_category_id: number;
|
product_category_id: number;
|
||||||
product_price: number | string;
|
product_price: number | string;
|
||||||
selling_price: number | string;
|
selling_price?: number | string;
|
||||||
tax: number | string;
|
tax?: number | string;
|
||||||
expiry_period: number | string;
|
expiry_period?: number | string;
|
||||||
supplier_ids: number[];
|
suppliers: {
|
||||||
|
supplier: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
price: number;
|
||||||
|
}[];
|
||||||
flags: string[];
|
flags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,7 +32,7 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
|
|||||||
Yup.object({
|
Yup.object({
|
||||||
name: Yup.string().required('Nama wajib diisi!'),
|
name: Yup.string().required('Nama wajib diisi!'),
|
||||||
brand: Yup.string().required('Merek wajib diisi!'),
|
brand: Yup.string().required('Merek wajib diisi!'),
|
||||||
sku: Yup.string().required('SKU wajib diisi!'),
|
sku: Yup.string(),
|
||||||
|
|
||||||
uom: Yup.object({
|
uom: Yup.object({
|
||||||
value: Yup.number()
|
value: Yup.number()
|
||||||
@@ -58,24 +64,34 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
|
|||||||
.min(1, 'Harga produk tidak boleh kurang dari 1!'),
|
.min(1, 'Harga produk tidak boleh kurang dari 1!'),
|
||||||
|
|
||||||
selling_price: Yup.number()
|
selling_price: Yup.number()
|
||||||
.required('Harga jual wajib diisi!')
|
.typeError('Harga hanya boleh angka!')
|
||||||
.typeError('Harga jual wajib diisi!')
|
|
||||||
.min(1, 'Harga jual tidak boleh kurang dari 1!'),
|
.min(1, 'Harga jual tidak boleh kurang dari 1!'),
|
||||||
|
|
||||||
tax: Yup.number()
|
tax: Yup.number()
|
||||||
.required('Pajak wajib diisi!')
|
.typeError('Pajak hanya boleh angka!')
|
||||||
.typeError('Pajak wajib diisi!')
|
|
||||||
.min(0, 'Pajak tidak boleh kurang dari 0!')
|
.min(0, 'Pajak tidak boleh kurang dari 0!')
|
||||||
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
|
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
|
||||||
|
|
||||||
expiry_period: Yup.number()
|
expiry_period: Yup.number()
|
||||||
.required('Periode kadaluarsa wajib diisi!')
|
.typeError('Periode kadaluarsa hanya boleh angka!')
|
||||||
.typeError('Periode kadaluarsa wajib diisi!')
|
|
||||||
.min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
|
.min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
|
||||||
|
|
||||||
supplier_ids: Yup.array()
|
suppliers: Yup.array()
|
||||||
.of(Yup.number().required().typeError('Supplier tidak valid!'))
|
.of(
|
||||||
.min(1, 'Minimal harus ada 1 supplier!')
|
Yup.object({
|
||||||
|
supplier: Yup.object({
|
||||||
|
value: Yup.number()
|
||||||
|
.min(1, 'Supplier wajib dipilih!')
|
||||||
|
.required('Supplier wajib dipilih!')
|
||||||
|
.typeError('Supplier wajib dipilih!'),
|
||||||
|
label: Yup.string().required('Supplier wajib dipilih!'),
|
||||||
|
}).required('Supplier wajib dipilih!'),
|
||||||
|
price: Yup.number()
|
||||||
|
.min(1, 'Harga tidak boleh kurang dari 1!')
|
||||||
|
.required('Harga wajib diisi!')
|
||||||
|
.typeError('Harga wajib diisi!'),
|
||||||
|
})
|
||||||
|
)
|
||||||
.required('Supplier wajib diisi!'),
|
.required('Supplier wajib diisi!'),
|
||||||
|
|
||||||
flags: Yup.array()
|
flags: Yup.array()
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ import { cn } from '@/lib/helper';
|
|||||||
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import { removeArrayItemAndSync } from '@/lib/utils/formik';
|
||||||
|
|
||||||
interface ProductFormProps {
|
interface ProductFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -101,7 +103,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
selling_price: initialValues?.selling_price ?? '',
|
selling_price: initialValues?.selling_price ?? '',
|
||||||
tax: initialValues?.tax ?? '',
|
tax: initialValues?.tax ?? '',
|
||||||
expiry_period: initialValues?.expiry_period ?? '',
|
expiry_period: initialValues?.expiry_period ?? '',
|
||||||
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [],
|
suppliers: initialValues?.suppliers
|
||||||
|
? initialValues.suppliers.map((supplier) => ({
|
||||||
|
supplier: {
|
||||||
|
value: supplier.id,
|
||||||
|
label: supplier.name,
|
||||||
|
},
|
||||||
|
price: supplier.price,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
flags: initialValues?.flags ?? [],
|
flags: initialValues?.flags ?? [],
|
||||||
}),
|
}),
|
||||||
[initialValues]
|
[initialValues]
|
||||||
@@ -120,12 +130,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
uom_id: values.uom_id,
|
uom_id: values.uom_id,
|
||||||
product_category_id: values.product_category_id,
|
product_category_id: values.product_category_id,
|
||||||
product_price: parseInt(values.product_price.toString()) || 0,
|
product_price: parseInt(values.product_price.toString()) || 0,
|
||||||
selling_price: parseInt(values.selling_price.toString()) || 0,
|
selling_price: values.selling_price
|
||||||
tax: parseInt(values.tax.toString()) || 0,
|
? parseInt(values.selling_price.toString()) || 0
|
||||||
expiry_period: parseInt(values.expiry_period.toString()) || 0,
|
: undefined,
|
||||||
supplier_ids: values.supplier_ids.filter(
|
tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
|
||||||
(id): id is number => typeof id === 'number'
|
expiry_period: values.expiry_period
|
||||||
),
|
? parseInt(values.expiry_period.toString()) || 0
|
||||||
|
: undefined,
|
||||||
|
suppliers: values.suppliers.map((s) => ({
|
||||||
|
supplier_id: s.supplier?.value as number,
|
||||||
|
price: parseInt(s.price.toString()) || 0,
|
||||||
|
})),
|
||||||
flags: values.flags.filter((f): f is string => typeof f === 'string'),
|
flags: values.flags.filter((f): f is string => typeof f === 'string'),
|
||||||
};
|
};
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -179,13 +194,29 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
category: 'SAPRONAK',
|
category: 'SAPRONAK',
|
||||||
});
|
});
|
||||||
|
|
||||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const filteredSupplierOptions = useMemo(() => {
|
||||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
return supplierOptions.filter((opt) => {
|
||||||
formik.setFieldTouched('supplier_ids', true);
|
return !formik.values.suppliers.some(
|
||||||
formik.setFieldValue(
|
(s) => s.supplier?.value === opt.value
|
||||||
'supplier_ids',
|
);
|
||||||
arr.map((v) => (v as OptionType).value)
|
});
|
||||||
);
|
}, [supplierOptions, formik.values.suppliers]);
|
||||||
|
|
||||||
|
const addSupplierHandler = () => {
|
||||||
|
formik.setFieldValue('suppliers', [
|
||||||
|
...formik.values.suppliers,
|
||||||
|
{
|
||||||
|
supplier_id: '',
|
||||||
|
price: formik.values.product_price,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSupplierItemHandler = (idx: number) => {
|
||||||
|
const path = 'suppliers';
|
||||||
|
|
||||||
|
// trims values, errors, and touched at idx
|
||||||
|
removeArrayItemAndSync(formik, path, idx);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProductClickHandler = () => {
|
const deleteProductClickHandler = () => {
|
||||||
@@ -201,6 +232,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
router.push('/master-data/product');
|
router.push('/master-data/product');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSupplierRepeaterError = (
|
||||||
|
column: 'supplier' | 'price',
|
||||||
|
supplierIdx: number
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
formik.touched.suppliers?.[supplierIdx]?.[column] &&
|
||||||
|
Boolean(
|
||||||
|
formik.errors.suppliers?.[supplierIdx] instanceof Object &&
|
||||||
|
formik.errors.suppliers?.[supplierIdx]?.[column]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formikSetValues(formikInitialValues);
|
formikSetValues(formikInitialValues);
|
||||||
}, [formikSetValues, formikInitialValues]);
|
}, [formikSetValues, formikInitialValues]);
|
||||||
@@ -271,7 +315,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
|
||||||
label='SKU'
|
label='SKU'
|
||||||
name='sku'
|
name='sku'
|
||||||
placeholder='Masukkan SKU...'
|
placeholder='Masukkan SKU...'
|
||||||
@@ -344,7 +387,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
label='Harga Jual'
|
label='Harga Jual'
|
||||||
name='selling_price'
|
name='selling_price'
|
||||||
placeholder='Masukkan harga jual...'
|
placeholder='Masukkan harga jual...'
|
||||||
@@ -366,7 +408,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
<div className='grid sm:grid-cols-2 gap-4'>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
label='Pajak (%)'
|
label='Pajak (%)'
|
||||||
name='tax'
|
name='tax'
|
||||||
placeholder='Masukkan pajak...'
|
placeholder='Masukkan pajak...'
|
||||||
@@ -383,7 +424,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
label='Periode Kadaluarsa (hari)'
|
label='Periode Kadaluarsa (hari)'
|
||||||
name='expiry_period'
|
name='expiry_period'
|
||||||
placeholder='Masukkan periode kadaluarsa...'
|
placeholder='Masukkan periode kadaluarsa...'
|
||||||
@@ -403,28 +443,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
<div className='grid sm:grid-cols-1 gap-4'>
|
||||||
<SelectInput
|
|
||||||
required
|
|
||||||
label='Supplier'
|
|
||||||
placeholder='Pilih supplier...'
|
|
||||||
isMulti
|
|
||||||
value={supplierOptions.filter((opt) =>
|
|
||||||
(formik.values.supplier_ids || []).includes(opt.value)
|
|
||||||
)}
|
|
||||||
onChange={supplierChangeHandler}
|
|
||||||
options={supplierOptions}
|
|
||||||
onInputChange={setSupplierSelectInputValue}
|
|
||||||
onMenuScrollToBottom={loadMoreSuppliers}
|
|
||||||
isLoading={isLoadingSuppliers}
|
|
||||||
isError={
|
|
||||||
formik.touched.supplier_ids &&
|
|
||||||
Boolean(formik.errors.supplier_ids)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.supplier_ids as string}
|
|
||||||
isDisabled={type === 'detail'}
|
|
||||||
isClearable
|
|
||||||
/>
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Flags'
|
label='Flags'
|
||||||
@@ -447,6 +466,129 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='grid sm:grid-cols-1 gap-4'>
|
||||||
|
{type !== 'detail' && formik.values.suppliers.length === 0 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addSupplierHandler}
|
||||||
|
className='w-fit mx-auto'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} /> Tambah
|
||||||
|
Supplier
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formik.values.suppliers.length > 0 && (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mb-4 text-center'>
|
||||||
|
<h4 className='font-bold text-xl'>Supplier</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
||||||
|
Supplier
|
||||||
|
</th>
|
||||||
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
||||||
|
Harga
|
||||||
|
</th>
|
||||||
|
<th>Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{formik.values.suppliers.map((supplier, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td className='p-2 w-full max-w-1/2'>
|
||||||
|
<SelectInput
|
||||||
|
placeholder='Pilih Supplier'
|
||||||
|
options={filteredSupplierOptions}
|
||||||
|
onInputChange={setSupplierSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isLoading={isLoadingSuppliers}
|
||||||
|
value={formik.values.suppliers[idx].supplier}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
`suppliers.${idx}.supplier`,
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isError={isSupplierRepeaterError(
|
||||||
|
'supplier',
|
||||||
|
idx
|
||||||
|
)}
|
||||||
|
isClearable
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'min-w-48 w-full',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className='p-2 w-full max-w-1/2'>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
name={`suppliers.${idx}.price`}
|
||||||
|
placeholder='Masukkan harga...'
|
||||||
|
value={formik.values.suppliers[idx].price}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
decimalScale={2}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputPrefix='Rp '
|
||||||
|
isError={isSupplierRepeaterError('price', idx)}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'min-w-48 w-full',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => deleteSupplierItemHandler(idx)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div className='w-full flex flex-row justify-center'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addSupplierHandler}
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />{' '}
|
||||||
|
Tambah Supplier
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ProductionStandard } from '@/types/api/master-data/production-standard'
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ProductionStandardApi } from '@/services/api/master-data';
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import { CellContext } from '@tanstack/react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
@@ -94,9 +94,16 @@ const ProductionStandardTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await ProductionStandardApi.delete(
|
const deleteResponse = await ProductionStandardApi.delete(
|
||||||
selectedProductionStandard?.id as number
|
selectedProductionStandard?.id as number
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshProductionStandards();
|
refreshProductionStandards();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
+15
-19
@@ -2,34 +2,30 @@ import * as Yup from 'yup';
|
|||||||
|
|
||||||
// Schema for LAYING category (production_standard_details is required)
|
// Schema for LAYING category (production_standard_details is required)
|
||||||
const LayingRepeaterFormSchema = Yup.object({
|
const LayingRepeaterFormSchema = Yup.object({
|
||||||
week: Yup.number().required('Minggu wajib diisi!'),
|
week: Yup.number().required('Wajib diisi!'),
|
||||||
production_standard_uniformity_details: Yup.object({
|
production_standard_uniformity_details: Yup.object({
|
||||||
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
target_mean_bw: Yup.number().required('Wajib diisi!'),
|
||||||
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
max_depletion: Yup.number().required('Wajib diisi!'),
|
||||||
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
min_uniformity: Yup.number().required('Wajib diisi!'),
|
||||||
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
feed_intake: Yup.number().required('Wajib diisi!'),
|
||||||
}),
|
}),
|
||||||
production_standard_details: Yup.object({
|
production_standard_details: Yup.object({
|
||||||
target_hen_day_production: Yup.number().required(
|
target_hen_day_production: Yup.number().required('Wajib diisi!'),
|
||||||
'Produksi telur per hari wajib diisi!'
|
target_hen_house_production: Yup.number().required('Wajib diisi!'),
|
||||||
),
|
target_egg_weight: Yup.number().required('Wajib diisi!'),
|
||||||
target_hen_house_production: Yup.number().required(
|
target_egg_mass: Yup.number().required('Wajib diisi!'),
|
||||||
'Produksi telur per kandang wajib diisi!'
|
standard_fcr: Yup.number().required('Wajib diisi!'),
|
||||||
),
|
|
||||||
target_egg_weight: Yup.number().required('Berat telur wajib diisi!'),
|
|
||||||
target_egg_mass: Yup.number().required('Massa telur wajib diisi!'),
|
|
||||||
standard_fcr: Yup.number().required('FCR wajib diisi!'),
|
|
||||||
}).required(),
|
}).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schema for GROWING category (production_standard_details is optional)
|
// Schema for GROWING category (production_standard_details is optional)
|
||||||
const GrowingRepeaterFormSchema = Yup.object({
|
const GrowingRepeaterFormSchema = Yup.object({
|
||||||
week: Yup.number().required('Minggu wajib diisi!'),
|
week: Yup.number().required('Wajib diisi!'),
|
||||||
production_standard_uniformity_details: Yup.object({
|
production_standard_uniformity_details: Yup.object({
|
||||||
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
target_mean_bw: Yup.number().required('Wajib diisi!'),
|
||||||
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
max_depletion: Yup.number().required('Wajib diisi!'),
|
||||||
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
min_uniformity: Yup.number().required('Wajib diisi!'),
|
||||||
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
feed_intake: Yup.number().required('Wajib diisi!'),
|
||||||
}),
|
}),
|
||||||
production_standard_details: Yup.object({
|
production_standard_details: Yup.object({
|
||||||
target_hen_day_production: Yup.number().optional(),
|
target_hen_day_production: Yup.number().optional(),
|
||||||
|
|||||||
+86
-45
@@ -344,7 +344,7 @@ const ProductionStandardForm = ({
|
|||||||
const columns = useMemo<ColumnDef<TableRowsType>[]>(() => {
|
const columns = useMemo<ColumnDef<TableRowsType>[]>(() => {
|
||||||
const baseColumns: ColumnDef<TableRowsType>[] = [
|
const baseColumns: ColumnDef<TableRowsType>[] = [
|
||||||
{
|
{
|
||||||
header: 'Minggu',
|
header: 'Week',
|
||||||
accessorKey: 'week',
|
accessorKey: 'week',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
@@ -358,30 +358,40 @@ const ProductionStandardForm = ({
|
|||||||
header: 'Hen Day',
|
header: 'Hen Day',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_hen_day_production,
|
row.production_standard_details?.target_hen_day_production,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_hen_day_production}%`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Hen House',
|
header: 'Hen House',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_hen_house_production,
|
row.production_standard_details?.target_hen_house_production,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_hen_house_production} pc`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Egg Weight',
|
header: 'Egg Weight',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_egg_weight,
|
row.production_standard_details?.target_egg_weight,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_egg_weight} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Egg Mass',
|
header: 'Egg Mass',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_egg_mass,
|
row.production_standard_details?.target_egg_mass,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_egg_mass} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'FCR',
|
header: 'FCR',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.standard_fcr,
|
row.production_standard_details?.standard_fcr,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.standard_fcr} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -393,24 +403,32 @@ const ProductionStandardForm = ({
|
|||||||
header: 'Mean BW',
|
header: 'Mean BW',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.target_mean_bw,
|
row.production_standard_uniformity_details?.target_mean_bw,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.target_mean_bw} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Max Depletion',
|
header: 'Max Depletion',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.max_depletion,
|
row.production_standard_uniformity_details?.max_depletion,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.max_depletion}%`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Min Uniformity',
|
header: 'Min Uniformity',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.min_uniformity,
|
row.production_standard_uniformity_details?.min_uniformity,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.min_uniformity}%`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Feed Intake',
|
header: 'Feed Intake',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.feed_intake,
|
row.production_standard_uniformity_details?.feed_intake,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.feed_intake} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -728,7 +746,52 @@ const ProductionStandardForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
||||||
|
formik,
|
||||||
|
{
|
||||||
|
onBeforeSubmit: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// For GROWING category, clear production_standard_details errors and set default values
|
||||||
|
if (formik.values.project_category === 'GROWING') {
|
||||||
|
// Set default values for production_standard_details
|
||||||
|
formik.values.details?.forEach((detail) => {
|
||||||
|
detail.production_standard_details = {
|
||||||
|
target_hen_day_production: 0,
|
||||||
|
target_hen_house_production: 0,
|
||||||
|
target_egg_weight: 0,
|
||||||
|
target_egg_mass: 0,
|
||||||
|
standard_fcr: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear any errors related to production_standard_details
|
||||||
|
const currentErrors = { ...formik.errors };
|
||||||
|
if (currentErrors.details && Array.isArray(currentErrors.details)) {
|
||||||
|
const cleanedDetails = currentErrors.details
|
||||||
|
.map((detailError) => {
|
||||||
|
if (detailError && typeof detailError === 'object') {
|
||||||
|
const { production_standard_details, ...rest } = detailError;
|
||||||
|
return Object.keys(rest).length > 0 ? rest : undefined;
|
||||||
|
}
|
||||||
|
return detailError;
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(error): error is Exclude<typeof error, undefined> =>
|
||||||
|
error !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
currentErrors.details = (
|
||||||
|
cleanedDetails.length > 0 ? cleanedDetails : undefined
|
||||||
|
) as typeof currentErrors.details;
|
||||||
|
}
|
||||||
|
formik.setErrors(currentErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -821,19 +884,20 @@ const ProductionStandardForm = ({
|
|||||||
key={`row-${row.index}`}
|
key={`row-${row.index}`}
|
||||||
className='sticky bottom-0 bg-base-100 shadow-lg'
|
className='sticky bottom-0 bg-base-100 shadow-lg'
|
||||||
>
|
>
|
||||||
<td colSpan={colSpan} className='p-6'>
|
<td colSpan={colSpan} className='p-2'>
|
||||||
<form
|
<form
|
||||||
className='h-full w-full flex flex-col justify-end'
|
className='h-full w-full flex flex-col justify-end'
|
||||||
onSubmit={repeaterFormik.handleSubmit}
|
onSubmit={repeaterFormik.handleSubmit}
|
||||||
onReset={repeaterFormik.handleReset}
|
onReset={repeaterFormik.handleReset}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className='grid gap-2 items-start w-full'
|
||||||
'grid gap-4 items-start',
|
style={{
|
||||||
formik.values.project_category === 'LAYING'
|
gridTemplateColumns:
|
||||||
? 'grid-cols-10'
|
formik.values.project_category === 'LAYING'
|
||||||
: 'grid-cols-5'
|
? 'repeat(10, minmax(auto, 1fr)) minmax(auto, auto)'
|
||||||
)}
|
: 'repeat(4, minmax(auto, 1fr)) minmax(auto, auto)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
name='week'
|
name='week'
|
||||||
@@ -862,7 +926,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={<Icon icon='mdi:percent' />}
|
bottomLabel='Persen (%)'
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -894,11 +958,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Butir (pc)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
Butir
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -930,11 +990,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -959,17 +1015,13 @@ const ProductionStandardForm = ({
|
|||||||
name='production_standard_details.target_egg_mass'
|
name='production_standard_details.target_egg_mass'
|
||||||
label='Egg Mass'
|
label='Egg Mass'
|
||||||
placeholder='1'
|
placeholder='1'
|
||||||
|
bottomLabel='Gram (g)'
|
||||||
value={
|
value={
|
||||||
repeaterFormik.values
|
repeaterFormik.values
|
||||||
.production_standard_details?.target_egg_mass
|
.production_standard_details?.target_egg_mass
|
||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -1000,11 +1052,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -1038,11 +1086,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1072,7 +1116,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={<Icon icon='mdi:percent' />}
|
bottomLabel='Persen (%)'
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1102,7 +1146,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={<Icon icon='mdi:percent' />}
|
bottomLabel='Persen (%)'
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1132,11 +1176,8 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram/Ekor (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
endAdornment
|
||||||
gr/ekor
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1162,7 +1203,7 @@ const ProductionStandardForm = ({
|
|||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='min-w-24'
|
className='min-w-xs'
|
||||||
onClick={handleCancelEdit}
|
onClick={handleCancelEdit}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:close' /> Batal
|
<Icon icon='mdi:close' /> Batal
|
||||||
@@ -1178,7 +1219,7 @@ const ProductionStandardForm = ({
|
|||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
color={editMode ? 'warning' : 'success'}
|
color={editMode ? 'warning' : 'success'}
|
||||||
className='min-w-24'
|
className='min-w-xs'
|
||||||
disabled={
|
disabled={
|
||||||
isAddingRow ||
|
isAddingRow ||
|
||||||
formik.values.project_category === ''
|
formik.values.project_category === ''
|
||||||
@@ -1195,7 +1236,7 @@ const ProductionStandardForm = ({
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
color='primary'
|
color='primary'
|
||||||
onClick={toggleTableHeight}
|
onClick={toggleTableHeight}
|
||||||
className='absolute bottom-6 right-6'
|
className='absolute bottom-2 right-2'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={
|
icon={
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -205,7 +205,16 @@ const SuppliersTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await SupplierApi.delete(selectedSupplier?.id as number);
|
const deleteResponse = await SupplierApi.delete(
|
||||||
|
selectedSupplier?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshSuppliers();
|
refreshSuppliers();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Uom } from '@/types/api/master-data/uom';
|
import { Uom } from '@/types/api/master-data/uom';
|
||||||
import { UomApi } from '@/services/api/master-data';
|
import { UomApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -164,7 +164,14 @@ const UomsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await UomApi.delete(selectedUom?.id as number);
|
const deleteResponse = await UomApi.delete(selectedUom?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshUoms();
|
refreshUoms();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
import { WarehouseApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -220,7 +220,16 @@ const WarehousesTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await WarehouseApi.delete(selectedWarehouse?.id as number);
|
const deleteResponse = await WarehouseApi.delete(
|
||||||
|
selectedWarehouse?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshWarehouses();
|
refreshWarehouses();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama lokasi'
|
placeholder='Masukkan nama warehouse'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
|
|||||||
@@ -75,12 +75,12 @@ const ChickinFormKandang = ({
|
|||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color='success'
|
color='primary'
|
||||||
className={{
|
className={{
|
||||||
badge: 'rounded-lg px-2',
|
badge: 'rounded-lg px-2',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:circle' width={12} height={12} color='success' />{' '}
|
<Icon icon='mdi:circle' width={12} height={12} color='primary' />{' '}
|
||||||
Aktif
|
Aktif
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
<div className='divider divider-horizontal p-0 m-0'></div>
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ import Button from '@/components/Button';
|
|||||||
import FloatingActionsButton from '@/components/FloatingActionsButton';
|
import FloatingActionsButton from '@/components/FloatingActionsButton';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
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, formatDate } from '@/lib/helper';
|
import { cn, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -59,9 +62,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
const selectedRowIds = Object.keys(rowSelection)
|
const selectedRowIds = Object.keys(rowSelection)
|
||||||
.filter((id) => rowSelection[id])
|
.filter((id) => rowSelection[id])
|
||||||
.map((id) => parseInt(id));
|
.map((id) => parseInt(id));
|
||||||
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
|
|
||||||
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
|
|
||||||
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
|
|
||||||
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
null
|
null
|
||||||
@@ -90,55 +90,25 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
{ revalidateOnMount: true }
|
{ revalidateOnMount: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
|
// ===== Fetch Data Select =====
|
||||||
search: areaSelectInputValue,
|
const {
|
||||||
limit: '100',
|
options: optionsArea,
|
||||||
}).toString()}`;
|
isLoadingOptions: isLoadingArea,
|
||||||
const { data: areas, isLoading: isLoadingAreas } = useSWR(
|
setInputValue: setAreaSelectInputValue,
|
||||||
areaUrl,
|
loadMore: loadMoreArea,
|
||||||
AreaApi.getAllFetcher
|
} = useSelect(AreaApi.basePath, 'id', 'name');
|
||||||
);
|
const {
|
||||||
|
options: optionsLocation,
|
||||||
const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({
|
isLoadingOptions: isLoadingLocation,
|
||||||
search: locationSelectInputValue,
|
setInputValue: setLocationSelectInputValue,
|
||||||
area_id: selectedArea != null ? selectedArea.value.toString() : '',
|
loadMore: loadMoreLocation,
|
||||||
limit: '100',
|
} = useSelect(LocationApi.basePath, 'id', 'name');
|
||||||
}).toString()}`;
|
const {
|
||||||
const { data: locations, isLoading: isLoadingLocations } = useSWR(
|
options: optionsKandang,
|
||||||
locationUrl,
|
isLoadingOptions: isLoadingKandang,
|
||||||
LocationApi.getAllFetcher
|
setInputValue: setKandangSelectInputValue,
|
||||||
);
|
loadMore: loadMoreKandang,
|
||||||
|
} = useSelect(KandangApi.basePath, 'id', 'name');
|
||||||
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
|
|
||||||
search: kandangSelectInputValue,
|
|
||||||
location_id:
|
|
||||||
selectedLocation != null ? selectedLocation.value.toString() : '',
|
|
||||||
limit: '100',
|
|
||||||
}).toString()}`;
|
|
||||||
const { data: kandangs, isLoading: isLoadingKandang } = useSWR(
|
|
||||||
kandangUrl,
|
|
||||||
KandangApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== Data to Options Mapping ======
|
|
||||||
const optionsArea = isResponseSuccess(areas)
|
|
||||||
? areas?.data.map((area) => ({
|
|
||||||
value: area.id,
|
|
||||||
label: area.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
const optionsKandang = isResponseSuccess(kandangs)
|
|
||||||
? kandangs?.data.map((kandang) => ({
|
|
||||||
value: kandang.id,
|
|
||||||
label: kandang.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
const optionsLocation = isResponseSuccess(locations)
|
|
||||||
? locations?.data.map((location) => ({
|
|
||||||
value: location.id,
|
|
||||||
label: location.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// ====== HANDLER ======
|
// ====== HANDLER ======
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -284,7 +254,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
header: 'Status',
|
header: 'Status',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.approval;
|
const approval = props.row.original.approval;
|
||||||
|
const isRejected = approval?.action == 'REJECTED';
|
||||||
|
const isApproved = approval?.action == 'APPROVED';
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
@@ -292,11 +263,17 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
badge: 'rounded-lg px-2 w-full flex flex-row justify-start',
|
badge: 'rounded-lg px-2 w-full flex flex-row justify-start',
|
||||||
}}
|
}}
|
||||||
color={
|
color={
|
||||||
approval?.step_number == 1
|
isRejected
|
||||||
? 'neutral'
|
? 'error'
|
||||||
: approval?.step_number == 2
|
: isApproved
|
||||||
? 'success'
|
? approval?.step_number == 1
|
||||||
: 'error'
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
: 'neutral'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -307,11 +284,15 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
approval?.step_number == 1
|
approval?.step_number == 1
|
||||||
? 'neutral'
|
? 'neutral'
|
||||||
: approval?.step_number == 2
|
: approval?.step_number == 2
|
||||||
? 'success'
|
? 'primary'
|
||||||
: 'error'
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{approval?.step_name}
|
{isRejected
|
||||||
|
? 'Ditolak'
|
||||||
|
: formatTitleCase(approval?.step_name || '')}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -385,7 +366,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
options={optionsArea}
|
options={optionsArea}
|
||||||
isLoading={isLoadingAreas}
|
isLoading={isLoadingArea}
|
||||||
value={selectedArea}
|
value={selectedArea}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setSelectedArea(val as OptionType);
|
setSelectedArea(val as OptionType);
|
||||||
@@ -395,12 +376,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onInputChange={setAreaSelectInputValue}
|
onInputChange={setAreaSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreArea}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
options={optionsLocation}
|
options={optionsLocation}
|
||||||
isLoading={isLoadingLocations}
|
isLoading={isLoadingLocation}
|
||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setSelectedLocation(val as OptionType);
|
setSelectedLocation(val as OptionType);
|
||||||
@@ -410,6 +392,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onInputChange={setLocationSelectInputValue}
|
onInputChange={setLocationSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocation}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -425,6 +408,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onInputChange={setKandangSelectInputValue}
|
onInputChange={setKandangSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandang}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ const ProjectFlockDetail = ({
|
|||||||
projectFlock.approval?.step_number == 1
|
projectFlock.approval?.step_number == 1
|
||||||
? 'neutral'
|
? 'neutral'
|
||||||
: projectFlock.approval?.step_number == 2
|
: projectFlock.approval?.step_number == 2
|
||||||
? 'success'
|
? 'primary'
|
||||||
: projectFlock.approval?.step_number >= 3
|
: projectFlock.approval?.step_number == 3
|
||||||
? 'error'
|
? 'success'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
className={{
|
className={{
|
||||||
@@ -173,9 +173,9 @@ const ProjectFlockDetail = ({
|
|||||||
projectFlock.approval?.step_number == 1
|
projectFlock.approval?.step_number == 1
|
||||||
? 'neutral'
|
? 'neutral'
|
||||||
: projectFlock.approval?.step_number == 2
|
: projectFlock.approval?.step_number == 2
|
||||||
? 'success'
|
? 'primary'
|
||||||
: projectFlock.approval?.step_number >= 3
|
: projectFlock.approval?.step_number == 3
|
||||||
? 'error'
|
? 'success'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
@@ -273,7 +273,7 @@ const ProjectFlockDetail = ({
|
|||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color={'success'}
|
color={'primary'}
|
||||||
className={{
|
className={{
|
||||||
badge: 'rounded-lg px-2',
|
badge: 'rounded-lg px-2',
|
||||||
}}
|
}}
|
||||||
@@ -282,7 +282,7 @@ const ProjectFlockDetail = ({
|
|||||||
icon='mdi:circle'
|
icon='mdi:circle'
|
||||||
width={12}
|
width={12}
|
||||||
height={12}
|
height={12}
|
||||||
color={'success'}
|
color={'primary'}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Kandang Aktif ({projectFlock.kandangs?.length})
|
Kandang Aktif ({projectFlock.kandangs?.length})
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -102,41 +102,54 @@ const ProjectFlockForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Fetch Data
|
// Fetch Data
|
||||||
const { isLoadingOptions: isLoadingFlocks, options: optionsFlock } =
|
const {
|
||||||
useSelect(FlockApi.basePath, 'id', 'name');
|
setInputValue: setInputValueFlock,
|
||||||
|
isLoadingOptions: isLoadingFlocks,
|
||||||
|
options: optionsFlock,
|
||||||
|
loadMore: loadMoreFlock,
|
||||||
|
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
||||||
|
project_category: selectedCategory,
|
||||||
|
});
|
||||||
|
|
||||||
const { options: optionsArea, isLoadingOptions: isLoadingAreas } = useSelect(
|
const {
|
||||||
AreaApi.basePath,
|
setInputValue: setInputValueArea,
|
||||||
'id',
|
options: optionsArea,
|
||||||
'name'
|
isLoadingOptions: isLoadingAreas,
|
||||||
);
|
loadMore: loadMoreArea,
|
||||||
|
} = useSelect(AreaApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const { options: optionsLocation, isLoadingOptions: isLoadingLocations } =
|
const {
|
||||||
useSelect(LocationApi.basePath, 'id', 'name', '', {
|
options: optionsLocation,
|
||||||
area_id:
|
isLoadingOptions: isLoadingLocations,
|
||||||
selectedArea != ''
|
setInputValue: setInputValueLocation,
|
||||||
? selectedArea
|
loadMore: loadMoreLocation,
|
||||||
: ((initialValues?.area?.id ?? '') as string),
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
});
|
area_id:
|
||||||
|
selectedArea != ''
|
||||||
|
? selectedArea
|
||||||
|
: ((initialValues?.area?.id ?? '') as string),
|
||||||
|
});
|
||||||
|
|
||||||
const { options: optionsFcr, isLoadingOptions: isLoadingFcrs } = useSelect(
|
const {
|
||||||
FcrApi.basePath,
|
options: optionsFcr,
|
||||||
'id',
|
isLoadingOptions: isLoadingFcrs,
|
||||||
'name'
|
setInputValue: setInputValueFcr,
|
||||||
);
|
loadMore: loadMoreFcr,
|
||||||
|
} = useSelect(FcrApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: optionsProductionStandards,
|
options: optionsProductionStandards,
|
||||||
isLoadingOptions: isLoadingProductionStandards,
|
isLoadingOptions: isLoadingProductionStandards,
|
||||||
|
setInputValue: setInputValueProductionStandard,
|
||||||
|
loadMore: loadMoreProductionStandard,
|
||||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
||||||
search: '',
|
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
|
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
|
||||||
search: '',
|
search: '',
|
||||||
location_id: selectedLocation == '' ? '0' : selectedLocation,
|
location_id: selectedLocation == '' ? '0' : selectedLocation,
|
||||||
limit: 'limit',
|
limit: '500',
|
||||||
}).toString()}`;
|
}).toString()}`;
|
||||||
const {
|
const {
|
||||||
data: kandang,
|
data: kandang,
|
||||||
@@ -153,6 +166,8 @@ const ProjectFlockForm = ({
|
|||||||
options: optionsNonstock,
|
options: optionsNonstock,
|
||||||
rawData: nonstocks,
|
rawData: nonstocks,
|
||||||
isLoadingOptions: isLoadingNonstocks,
|
isLoadingOptions: isLoadingNonstocks,
|
||||||
|
setInputValue: setInputValueNonstock,
|
||||||
|
loadMore: loadMoreNonstock,
|
||||||
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -542,15 +557,12 @@ const ProjectFlockForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDeleteBudgetRowHandler = (nonstock_id: number, index?: number) => {
|
const onDeleteBudgetRowHandler = (nonstock_id: number, index?: number) => {
|
||||||
console.log(`nonstock_id: ${nonstock_id}, index: ${index}`);
|
|
||||||
if (!nonstock_id) {
|
if (!nonstock_id) {
|
||||||
const updatedBudgets = formik.values.project_budgets
|
const updatedBudgets = formik.values.project_budgets
|
||||||
.map((budget, i) => {
|
.map((budget, i) => {
|
||||||
if (i == index) {
|
if (i == index) {
|
||||||
console.log(`buget: ${null}, index: ${index}, i: ${i}`);
|
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
console.log(`buget: ${budget}, index: ${index}, i: ${i}`);
|
|
||||||
return budget;
|
return budget;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -722,6 +734,8 @@ const ProjectFlockForm = ({
|
|||||||
formik.touched.area_id && Boolean(formik.errors.area_id)
|
formik.touched.area_id && Boolean(formik.errors.area_id)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.area_id as string}
|
errorMessage={formik.errors.area_id as string}
|
||||||
|
onInputChange={setInputValueArea}
|
||||||
|
onMenuScrollToBottom={loadMoreArea}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={formType != 'add'}
|
isDisabled={formType != 'add'}
|
||||||
/>
|
/>
|
||||||
@@ -740,6 +754,8 @@ const ProjectFlockForm = ({
|
|||||||
formik.touched.location_id &&
|
formik.touched.location_id &&
|
||||||
Boolean(formik.errors.location_id)
|
Boolean(formik.errors.location_id)
|
||||||
}
|
}
|
||||||
|
onInputChange={setInputValueLocation}
|
||||||
|
onMenuScrollToBottom={loadMoreLocation}
|
||||||
errorMessage={formik.errors.location_id as string}
|
errorMessage={formik.errors.location_id as string}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={formType != 'add' || disabledLocation}
|
isDisabled={formType != 'add' || disabledLocation}
|
||||||
@@ -766,6 +782,8 @@ const ProjectFlockForm = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={optionsFlock}
|
options={optionsFlock}
|
||||||
|
onInputChange={setInputValueFlock}
|
||||||
|
onMenuScrollToBottom={loadMoreFlock}
|
||||||
isLoading={isLoadingFlocks}
|
isLoading={isLoadingFlocks}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.flock_name && Boolean(formik.errors.flock_name)
|
formik.touched.flock_name && Boolean(formik.errors.flock_name)
|
||||||
@@ -781,6 +799,8 @@ const ProjectFlockForm = ({
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
optionChangeHandler(val, 'fcr');
|
optionChangeHandler(val, 'fcr');
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setInputValueFcr}
|
||||||
|
onMenuScrollToBottom={loadMoreFcr}
|
||||||
options={optionsFcr}
|
options={optionsFcr}
|
||||||
isLoading={isLoadingFcrs}
|
isLoading={isLoadingFcrs}
|
||||||
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
|
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
|
||||||
@@ -808,6 +828,8 @@ const ProjectFlockForm = ({
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
optionChangeHandler(val, 'production_standard');
|
optionChangeHandler(val, 'production_standard');
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setInputValueProductionStandard}
|
||||||
|
onMenuScrollToBottom={loadMoreProductionStandard}
|
||||||
options={optionsProductionStandards}
|
options={optionsProductionStandards}
|
||||||
isLoading={isLoadingProductionStandards}
|
isLoading={isLoadingProductionStandards}
|
||||||
isError={
|
isError={
|
||||||
@@ -892,6 +914,8 @@ const ProjectFlockForm = ({
|
|||||||
isLoading={isLoadingNonstocks}
|
isLoading={isLoadingNonstocks}
|
||||||
placeholder='Pilih barang non stock'
|
placeholder='Pilih barang non stock'
|
||||||
value={formik.values.project_budgets[index].nonstock}
|
value={formik.values.project_budgets[index].nonstock}
|
||||||
|
onInputChange={setInputValueNonstock}
|
||||||
|
onMenuScrollToBottom={loadMoreNonstock}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
const updatedBudgets = [
|
const updatedBudgets = [
|
||||||
...formik.values.project_budgets,
|
...formik.values.project_budgets,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { RefObject } from 'react';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { SortingState, CellContext } from '@tanstack/react-table';
|
import { SortingState, CellContext } from '@tanstack/react-table';
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
@@ -656,30 +656,52 @@ const RecordingTable = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
const recording = row.original;
|
||||||
|
const isDisabled = isRecordingApproved(recording);
|
||||||
|
|
||||||
|
const handleToggleSelection = (e: unknown) => {
|
||||||
|
if (!isDisabled) {
|
||||||
|
row.getToggleSelectedHandler()(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={cn({ 'opacity-50': isDisabled })}>
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name='row'
|
name='row'
|
||||||
checked={row.getIsSelected()}
|
checked={row.getIsSelected()}
|
||||||
indeterminate={row.getIsSomeSelected()}
|
indeterminate={row.getIsSomeSelected()}
|
||||||
onChange={row.getToggleSelectedHandler()}
|
onChange={handleToggleSelection}
|
||||||
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: '#',
|
header: 'No',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||||
props.row.index +
|
props.row.index +
|
||||||
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',
|
||||||
|
cell: (props) => props.row.original.project_flock?.period || '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
@@ -696,19 +718,280 @@ const RecordingTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Umur (hari)',
|
header: 'Umur (hari)',
|
||||||
cell: (props) => props.row.original.day,
|
cell: (props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{props.row.original.day} (Minggu ke-
|
||||||
|
{props.row.original.project_flock.production_standart.week})
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Populasi Awal',
|
header: 'Populasi Akhir',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
props.row.original.project_flock?.total_chick_qty?.toLocaleString() ||
|
props.row.original.project_flock?.total_chick_qty != null
|
||||||
'-',
|
? formatNumber(props.row.original.project_flock.total_chick_qty)
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fcr',
|
||||||
|
header: 'FCR',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'fcr_actual',
|
||||||
|
header: 'Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.fcr_value;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fcr_standard',
|
||||||
|
header: 'Standard',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.project_flock?.fcr?.fcr_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'feed_intake',
|
||||||
|
header: 'Feed Intake (KG)',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'feed_intake_actual',
|
||||||
|
header: 'Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.feed_intake;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'feed_intake_standard',
|
||||||
|
header: 'Standard',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.feed_intake_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mortality',
|
||||||
|
header: 'Mortality',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'cum_depletion_rate_actual',
|
||||||
|
header: 'Cum Depletion Rate',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.cum_depletion_rate;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'max_depletion_std',
|
||||||
|
header: 'Max Depletion Std',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.max_depletion_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'total_depletion',
|
||||||
|
header: 'Total Depletion',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.total_depletion_qty;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_production',
|
||||||
|
header: 'Egg Production',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'egg_mass_actual',
|
||||||
|
header: 'Egg Mass Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_mass;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_mass_standard',
|
||||||
|
header: 'Egg Mass Standar',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.egg_mass_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_weight_actual',
|
||||||
|
header: 'Egg Weight Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_weight;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_weight_standard',
|
||||||
|
header: 'Egg Weight Standar',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.egg_weight_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hen_performance',
|
||||||
|
header: 'Hen Performance',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'hen_day_actual',
|
||||||
|
header: 'Hen Day Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.hen_day;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hen_day_standard',
|
||||||
|
header: 'Hen Day Standar',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.hen_day_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hen_house_actual',
|
||||||
|
header: 'Hen House Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.hen_house;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hen_house_standard',
|
||||||
|
header: 'Hen House Standar',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.hen_house_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status Approval',
|
header: 'Status Approval',
|
||||||
@@ -730,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'
|
||||||
@@ -755,7 +1023,7 @@ const RecordingTable = () => {
|
|||||||
}}
|
}}
|
||||||
onClick={openApprovalHistory}
|
onClick={openApprovalHistory}
|
||||||
>
|
>
|
||||||
{getStatusText(approval.action)}
|
{approval.step_name || approval.action}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -874,14 +1142,15 @@ const RecordingTable = () => {
|
|||||||
'mb-20':
|
'mb-20':
|
||||||
isResponseSuccess(recordings) && recordings?.data?.length === 0,
|
isResponseSuccess(recordings) && recordings?.data?.length === 0,
|
||||||
}),
|
}),
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
tableWrapperClassName: 'overflow-x-auto',
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
tableClassName: 'w-full table-auto text-sm',
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
headerColumnClassName:
|
headerColumnClassName:
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
bodyRowClassName:
|
||||||
|
'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200',
|
||||||
bodyColumnClassName:
|
bodyColumnClassName:
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ import {
|
|||||||
} from '@/types/api/production/recording';
|
} from '@/types/api/production/recording';
|
||||||
|
|
||||||
type RecordingGrowingFormSchemaType = {
|
type RecordingGrowingFormSchemaType = {
|
||||||
|
record_date: string;
|
||||||
|
location?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
location_id: number;
|
||||||
|
project_flock?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
project_flock_id: number;
|
||||||
|
kandang?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
kandang_id: number;
|
||||||
project_flock_kandang: {
|
project_flock_kandang: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -17,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;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,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({
|
||||||
@@ -59,32 +75,51 @@ 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> =
|
||||||
Yup.object({
|
Yup.object({
|
||||||
|
record_date: Yup.string()
|
||||||
|
.required('Tanggal recording wajib diisi!')
|
||||||
|
.min(1, 'Tanggal recording wajib diisi!')
|
||||||
|
.typeError('Tanggal recording wajib diisi!'),
|
||||||
|
location: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
location_id: Yup.number()
|
||||||
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
|
.required('Lokasi wajib diisi!')
|
||||||
|
.typeError('Lokasi wajib diisi!'),
|
||||||
|
project_flock: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_id: Yup.number()
|
||||||
|
.min(1, 'Project flock wajib diisi!')
|
||||||
|
.required('Project flock wajib diisi!')
|
||||||
|
.typeError('Project flock wajib diisi!'),
|
||||||
|
kandang: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
kandang_id: Yup.number()
|
||||||
|
.min(1, 'Kandang wajib diisi!')
|
||||||
|
.required('Kandang wajib diisi!')
|
||||||
|
.typeError('Kandang wajib diisi!'),
|
||||||
project_flock_kandang: Yup.object({
|
project_flock_kandang: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
@@ -100,7 +135,7 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
|
|||||||
.required('Project Flock Kandang wajib diisi!')
|
.required('Project Flock Kandang wajib diisi!')
|
||||||
.test(
|
.test(
|
||||||
'not-already-recorded',
|
'not-already-recorded',
|
||||||
'Project Flock ini sudah direcord hari ini!',
|
'Project Flock ini sudah direcord pada tanggal tersebut!',
|
||||||
function (value) {
|
function (value) {
|
||||||
const recordedProjectFlockIds = this.options.context
|
const recordedProjectFlockIds = this.options.context
|
||||||
?.recordedProjectFlockIds as Set<number>;
|
?.recordedProjectFlockIds as Set<number>;
|
||||||
@@ -119,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 =
|
||||||
@@ -179,6 +208,15 @@ type RecordingFormData = Partial<Recording> & {
|
|||||||
export const getRecordingGrowingFormInitialValues = (
|
export const getRecordingGrowingFormInitialValues = (
|
||||||
initialValues?: RecordingFormData
|
initialValues?: RecordingFormData
|
||||||
): RecordingGrowingFormValues => ({
|
): RecordingGrowingFormValues => ({
|
||||||
|
record_date: initialValues?.record_datetime
|
||||||
|
? new Date(initialValues.record_datetime).toISOString().split('T')[0]
|
||||||
|
: new Date().toISOString().split('T')[0],
|
||||||
|
location: null,
|
||||||
|
location_id: 0,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_id: 0,
|
||||||
|
kandang: null,
|
||||||
|
kandang_id: 0,
|
||||||
project_flock_kandang: initialValues?.project_flock_kandang_id
|
project_flock_kandang: initialValues?.project_flock_kandang_id
|
||||||
? {
|
? {
|
||||||
value: initialValues.project_flock_kandang_id,
|
value: initialValues.project_flock_kandang_id,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -179,12 +179,16 @@ const TransferToLayingsTable = () => {
|
|||||||
setInputValue: setFlockSourceInputValue,
|
setInputValue: setFlockSourceInputValue,
|
||||||
options: flockSourceOptions,
|
options: flockSourceOptions,
|
||||||
isLoadingOptions: isLoadingFlockSourceOptions,
|
isLoadingOptions: isLoadingFlockSourceOptions,
|
||||||
|
loadMore: loadMoreFlockSource,
|
||||||
|
hasMore: hasMoreFlockSource,
|
||||||
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setFlockDestinationInputValue,
|
setInputValue: setFlockDestinationInputValue,
|
||||||
options: flockDestinationOptions,
|
options: flockDestinationOptions,
|
||||||
isLoadingOptions: isLoadingFlockDestinationOptions,
|
isLoadingOptions: isLoadingFlockDestinationOptions,
|
||||||
|
loadMore: loadMoreFlockDestination,
|
||||||
|
hasMore: hasMoreFlockDestination,
|
||||||
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// Flocks value
|
// Flocks value
|
||||||
@@ -595,6 +599,7 @@ const TransferToLayingsTable = () => {
|
|||||||
value={selectedFlockSource}
|
value={selectedFlockSource}
|
||||||
onChange={flockSourceChangeHandler}
|
onChange={flockSourceChangeHandler}
|
||||||
onInputChange={setFlockSourceInputValue}
|
onInputChange={setFlockSourceInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockSource}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
wrapper: 'col-span-12 sm:col-span-3',
|
||||||
@@ -608,6 +613,7 @@ const TransferToLayingsTable = () => {
|
|||||||
value={selectedFlockDestination}
|
value={selectedFlockDestination}
|
||||||
onChange={flockDestinationChangeHandler}
|
onChange={flockDestinationChangeHandler}
|
||||||
onInputChange={setFlockDestinationInputValue}
|
onInputChange={setFlockDestinationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockDestination}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
wrapper: 'col-span-12 sm:col-span-3',
|
||||||
|
|||||||
@@ -270,6 +270,8 @@ const TransferToLayingForm = ({
|
|||||||
options: flockSourceOptions,
|
options: flockSourceOptions,
|
||||||
isLoadingOptions: isLoadingFlockSourceOptions,
|
isLoadingOptions: isLoadingFlockSourceOptions,
|
||||||
rawData: flockSources,
|
rawData: flockSources,
|
||||||
|
loadMore: loadMoreFlockSource,
|
||||||
|
hasMore: hasMoreFlockSource,
|
||||||
} = useSelect<ProjectFlock>(
|
} = useSelect<ProjectFlock>(
|
||||||
'/production/project-flocks',
|
'/production/project-flocks',
|
||||||
'id',
|
'id',
|
||||||
@@ -360,6 +362,8 @@ const TransferToLayingForm = ({
|
|||||||
options: flockDestinationOptions,
|
options: flockDestinationOptions,
|
||||||
isLoadingOptions: isLoadingFlockDestinationOptions,
|
isLoadingOptions: isLoadingFlockDestinationOptions,
|
||||||
rawData: flockDestinations,
|
rawData: flockDestinations,
|
||||||
|
loadMore: loadMoreFlockDestination,
|
||||||
|
hasMore: hasMoreFlockDestination,
|
||||||
} = useSelect<ProjectFlock>(
|
} = useSelect<ProjectFlock>(
|
||||||
'/production/project-flocks',
|
'/production/project-flocks',
|
||||||
'id',
|
'id',
|
||||||
@@ -573,6 +577,7 @@ const TransferToLayingForm = ({
|
|||||||
onChange={flockSourceChangeHandler}
|
onChange={flockSourceChangeHandler}
|
||||||
isLoading={isLoadingFlockSourceOptions}
|
isLoading={isLoadingFlockSourceOptions}
|
||||||
onInputChange={setFlockSourceInputValue}
|
onInputChange={setFlockSourceInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockSource}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.flockSource &&
|
formik.touched.flockSource &&
|
||||||
Boolean(typeof formik.errors.flockSource === 'string')
|
Boolean(typeof formik.errors.flockSource === 'string')
|
||||||
@@ -591,6 +596,7 @@ const TransferToLayingForm = ({
|
|||||||
onChange={flockDestinationChangeHandler}
|
onChange={flockDestinationChangeHandler}
|
||||||
isLoading={isLoadingFlockDestinationOptions}
|
isLoading={isLoadingFlockDestinationOptions}
|
||||||
onInputChange={setFlockDestinationInputValue}
|
onInputChange={setFlockDestinationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockDestination}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.flockDestination &&
|
formik.touched.flockDestination &&
|
||||||
Boolean(typeof formik.errors.flockDestination === 'string')
|
Boolean(typeof formik.errors.flockDestination === 'string')
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ import DateInput from '@/components/input/DateInput';
|
|||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
import {
|
||||||
|
ProjectFlockKandangLookup,
|
||||||
|
ProjectFlock,
|
||||||
|
} from '@/types/api/production/project-flock';
|
||||||
import {
|
import {
|
||||||
getStatusColor,
|
getStatusColor,
|
||||||
getStatusIndicatorColor,
|
getStatusIndicatorColor,
|
||||||
@@ -229,63 +232,37 @@ const UniformityTable = () => {
|
|||||||
useState<number | undefined>(undefined);
|
useState<number | undefined>(undefined);
|
||||||
const [filterStartDate, setFilterStartDate] = useState('');
|
const [filterStartDate, setFilterStartDate] = useState('');
|
||||||
const [filterEndDate, setFilterEndDate] = useState('');
|
const [filterEndDate, setFilterEndDate] = useState('');
|
||||||
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
|
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
|
||||||
|
useState<string>('');
|
||||||
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
|
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setFilterLocationInputValue,
|
setInputValue: setFilterLocationInputValue,
|
||||||
options: filterLocationOptions,
|
options: filterLocationOptions,
|
||||||
isLoadingOptions: isLoadingFilterLocations,
|
isLoadingOptions: isLoadingFilterLocations,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
loadMore: loadMoreFilterLocations,
|
||||||
limit: '100',
|
hasMore: hasMoreFilterLocations,
|
||||||
});
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
// ===== FETCH PROJECT FLOCKS DATA FOR FILTER =====
|
// ===== FETCH PROJECT FLOCKS DATA FOR FILTER =====
|
||||||
const filterProjectFlocksUrl = useMemo(() => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
search: projectFlockSearchValue || '',
|
|
||||||
limit: '100',
|
|
||||||
});
|
|
||||||
if (filterLocation) {
|
|
||||||
params.append('location_id', filterLocation.value.toString());
|
|
||||||
}
|
|
||||||
return `${ProjectFlockApi.basePath}?${params.toString()}`;
|
|
||||||
}, [projectFlockSearchValue, filterLocation]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: filterProjectFlocksData,
|
setInputValue: setFilterProjectFlockSearchValue,
|
||||||
isLoading: isLoadingFilterProjectFlocks,
|
options: filterProjectFlockOptions,
|
||||||
} = useSWR(filterProjectFlocksUrl, ProjectFlockApi.getAllFetcher);
|
rawData: filterProjectFlocksRawData,
|
||||||
|
isLoadingOptions: isLoadingFilterProjectFlocks,
|
||||||
const filterProjectFlocksDataList = useMemo(
|
loadMore: loadMoreFilterProjectFlocks,
|
||||||
() =>
|
hasMore: hasMoreFilterProjectFlocks,
|
||||||
isResponseSuccess(filterProjectFlocksData)
|
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
|
||||||
? filterProjectFlocksData.data
|
location_id: filterProjectFlockLocationId,
|
||||||
: undefined,
|
});
|
||||||
[filterProjectFlocksData]
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterProjectFlockOptions = useMemo(() => {
|
|
||||||
let options: OptionType[] = [];
|
|
||||||
|
|
||||||
if (isResponseSuccess(filterProjectFlocksData)) {
|
|
||||||
const flockOptions =
|
|
||||||
filterProjectFlocksData?.data.map((projectFlock) => ({
|
|
||||||
value: projectFlock.id,
|
|
||||||
label: projectFlock.flock_name || '',
|
|
||||||
})) || [];
|
|
||||||
options = options.concat(flockOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}, [filterProjectFlocksData]);
|
|
||||||
|
|
||||||
// ===== KANDANG OPTIONS FOR FILTER =====
|
// ===== KANDANG OPTIONS FOR FILTER =====
|
||||||
const filterKandangOptions = useMemo(() => {
|
const filterKandangOptions = useMemo(() => {
|
||||||
let options: OptionType[] = [];
|
let options: OptionType[] = [];
|
||||||
|
|
||||||
if (filterProjectFlock && filterProjectFlocksDataList) {
|
if (filterProjectFlock && isResponseSuccess(filterProjectFlocksRawData)) {
|
||||||
const selectedProjectFlockData = filterProjectFlocksDataList.find(
|
const data = filterProjectFlocksRawData.data as unknown as ProjectFlock[];
|
||||||
|
const selectedProjectFlockData = data.find(
|
||||||
(pf) => pf.id === filterProjectFlock.value
|
(pf) => pf.id === filterProjectFlock.value
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -301,7 +278,7 @@ const UniformityTable = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [filterProjectFlock, filterProjectFlocksDataList]);
|
}, [filterProjectFlock, filterProjectFlocksRawData]);
|
||||||
|
|
||||||
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
||||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||||
@@ -394,9 +371,13 @@ const UniformityTable = () => {
|
|||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterLocationChange = useCallback(
|
const handleFilterLocationChange = useCallback(
|
||||||
(val: OptionType | OptionType[] | null) => {
|
(val: OptionType | OptionType[] | null) => {
|
||||||
setFilterLocation(val as OptionType | null);
|
const location = val as OptionType | null;
|
||||||
|
setFilterLocation(location);
|
||||||
setFilterProjectFlock(null);
|
setFilterProjectFlock(null);
|
||||||
setFilterKandang(null);
|
setFilterKandang(null);
|
||||||
|
setFilterProjectFlockLocationId(
|
||||||
|
location ? location.value.toString() : ''
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -1206,6 +1187,7 @@ const UniformityTable = () => {
|
|||||||
options={filterLocationOptions}
|
options={filterLocationOptions}
|
||||||
onInputChange={setFilterLocationInputValue}
|
onInputChange={setFilterLocationInputValue}
|
||||||
isLoading={isLoadingFilterLocations}
|
isLoading={isLoadingFilterLocations}
|
||||||
|
onMenuScrollToBottom={loadMoreFilterLocations}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
{filterErrors.location && (
|
{filterErrors.location && (
|
||||||
@@ -1225,8 +1207,9 @@ const UniformityTable = () => {
|
|||||||
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
|
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
|
||||||
}}
|
}}
|
||||||
options={filterProjectFlockOptions}
|
options={filterProjectFlockOptions}
|
||||||
onInputChange={setProjectFlockSearchValue}
|
onInputChange={setFilterProjectFlockSearchValue}
|
||||||
isLoading={isLoadingFilterProjectFlocks}
|
isLoading={isLoadingFilterProjectFlocks}
|
||||||
|
onMenuScrollToBottom={loadMoreFilterProjectFlocks}
|
||||||
isDisabled={!filterLocation}
|
isDisabled={!filterLocation}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Badge from '../../../../Badge';
|
import Badge from '@/components/Badge';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { formatNumber } from '@/lib/helper';
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ import {
|
|||||||
VerifyUniformityPayload,
|
VerifyUniformityPayload,
|
||||||
} from '@/types/api/production/uniformity';
|
} from '@/types/api/production/uniformity';
|
||||||
import { type BaseApiResponse } from '@/types/api/api-general';
|
import { type BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
import {
|
||||||
|
ProjectFlockKandangLookup,
|
||||||
|
ProjectFlock,
|
||||||
|
} from '@/types/api/production/project-flock';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
||||||
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
||||||
@@ -88,7 +91,9 @@ const UniformityForm = ({
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
|
const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] =
|
||||||
|
useState<string>('');
|
||||||
|
|
||||||
const [selectedProjectFlock, setSelectedProjectFlock] =
|
const [selectedProjectFlock, setSelectedProjectFlock] =
|
||||||
useState<OptionType | null>(null);
|
useState<OptionType | null>(null);
|
||||||
|
|
||||||
@@ -100,50 +105,21 @@ const UniformityForm = ({
|
|||||||
setInputValue: setLocationSelectInputValue,
|
setInputValue: setLocationSelectInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
loadMore: loadMoreLocations,
|
||||||
page: '1',
|
hasMore: hasMoreLocations,
|
||||||
limit: '100',
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProjectFlockSearchValue,
|
||||||
|
options: projectFlockOptions,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
isLoadingOptions: isLoadingProjectFlocks,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
hasMore: hasMoreProjectFlocks,
|
||||||
|
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
|
||||||
|
location_id: selectedProjectFlockLocationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FETCH PROJECT FLOCKS DATA =====
|
|
||||||
const projectFlocksUrl = useMemo(() => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
search: projectFlockSearchValue || '',
|
|
||||||
page: '1',
|
|
||||||
limit: '100',
|
|
||||||
});
|
|
||||||
if (selectedLocation) {
|
|
||||||
params.append('location_id', selectedLocation.value.toString());
|
|
||||||
}
|
|
||||||
return `${ProjectFlockApi.basePath}?${params.toString()}`;
|
|
||||||
}, [projectFlockSearchValue, selectedLocation]);
|
|
||||||
|
|
||||||
const { data: projectFlocksData, isLoading: isLoadingProjectFlocks } = useSWR(
|
|
||||||
projectFlocksUrl,
|
|
||||||
ProjectFlockApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectFlocksDataList =
|
|
||||||
projectFlocksData?.status === 'success'
|
|
||||||
? projectFlocksData.data
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// ===== PROJECT FLOCK OPTIONS =====
|
|
||||||
const projectFlockOptions = useMemo(() => {
|
|
||||||
let options: OptionType[] = [];
|
|
||||||
|
|
||||||
if (isResponseSuccess(projectFlocksData)) {
|
|
||||||
const flockOptions =
|
|
||||||
projectFlocksData?.data.map((projectFlock) => ({
|
|
||||||
value: projectFlock.id,
|
|
||||||
label: projectFlock.flock_name || '',
|
|
||||||
})) || [];
|
|
||||||
options = options.concat(flockOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}, [projectFlocksData]);
|
|
||||||
|
|
||||||
// ===== APPROVED PROJECT FLOCK KANDANGS =====
|
// ===== APPROVED PROJECT FLOCK KANDANGS =====
|
||||||
const approvedProjectFlockKandangsUrl = useMemo(() => {
|
const approvedProjectFlockKandangsUrl = useMemo(() => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -168,8 +144,9 @@ const UniformityForm = ({
|
|||||||
const kandangOptions = useMemo(() => {
|
const kandangOptions = useMemo(() => {
|
||||||
let options: OptionType[] = [];
|
let options: OptionType[] = [];
|
||||||
|
|
||||||
if (selectedProjectFlock && projectFlocksDataList) {
|
if (selectedProjectFlock && isResponseSuccess(projectFlocksRawData)) {
|
||||||
const selectedProjectFlockData = projectFlocksDataList.find(
|
const data = projectFlocksRawData.data as unknown as ProjectFlock[];
|
||||||
|
const selectedProjectFlockData = data.find(
|
||||||
(pf) => pf.id === selectedProjectFlock.value
|
(pf) => pf.id === selectedProjectFlock.value
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -196,7 +173,7 @@ const UniformityForm = ({
|
|||||||
return options;
|
return options;
|
||||||
}, [
|
}, [
|
||||||
selectedProjectFlock,
|
selectedProjectFlock,
|
||||||
projectFlocksDataList,
|
projectFlocksRawData,
|
||||||
approvedProjectFlockKandangs,
|
approvedProjectFlockKandangs,
|
||||||
formType,
|
formType,
|
||||||
]);
|
]);
|
||||||
@@ -313,6 +290,10 @@ const UniformityForm = ({
|
|||||||
formik.setFieldValue('location_id', locationId);
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
|
||||||
setSelectedLocation(location);
|
setSelectedLocation(location);
|
||||||
|
setSelectedProjectFlock(null);
|
||||||
|
setSelectedProjectFlockLocationId(
|
||||||
|
location ? location.value.toString() : ''
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -513,6 +494,7 @@ const UniformityForm = ({
|
|||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
onInputChange={setLocationSelectInputValue}
|
onInputChange={setLocationSelectInputValue}
|
||||||
isLoading={isLoadingLocations}
|
isLoading={isLoadingLocations}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.location_id && Boolean(formik.errors.location_id)
|
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||||
}
|
}
|
||||||
@@ -530,6 +512,7 @@ const UniformityForm = ({
|
|||||||
options={projectFlockOptions}
|
options={projectFlockOptions}
|
||||||
onInputChange={setProjectFlockSearchValue}
|
onInputChange={setProjectFlockSearchValue}
|
||||||
isLoading={isLoadingProjectFlocks}
|
isLoading={isLoadingProjectFlocks}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isDisabled={!formik.values.location_id}
|
isDisabled={!formik.values.location_id}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.project_flock_id &&
|
formik.touched.project_flock_id &&
|
||||||
|
|||||||
@@ -156,8 +156,11 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
setInputValue: setExpeditionsSelectInputValue,
|
setInputValue: setExpeditionsSelectInputValue,
|
||||||
options: expeditionVendors,
|
options: expeditionVendors,
|
||||||
isLoadingOptions: isLoadingExpeditions,
|
isLoadingOptions: isLoadingExpeditions,
|
||||||
|
loadMore: loadMoreExpeditions,
|
||||||
|
hasMore: hasMoreExpeditions,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
|
||||||
category: 'BOP',
|
category: 'BOP',
|
||||||
|
flag: 'EKSPEDISI',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FORM CONFIGURATION =====
|
// ===== FORM CONFIGURATION =====
|
||||||
@@ -183,8 +186,8 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
purchase_item_id: formItem.purchase_item_id || 0,
|
purchase_item_id: formItem.purchase_item_id || 0,
|
||||||
received_date: formItem.received_date || '',
|
received_date: formItem.received_date || '',
|
||||||
travel_number: formItem.travel_number || '',
|
travel_number: formItem.travel_number || '',
|
||||||
vehicle_number: formItem.vehicle_number || '',
|
vehicle_number: formItem.vehicle_number || null,
|
||||||
expedition_vendor_id: formItem.expedition_vendor_id || 0,
|
expedition_vendor_id: formItem.expedition_vendor_id || null,
|
||||||
received_qty:
|
received_qty:
|
||||||
typeof formItem.received_qty === 'string'
|
typeof formItem.received_qty === 'string'
|
||||||
? parseFloat(formItem.received_qty) || 0
|
? parseFloat(formItem.received_qty) || 0
|
||||||
@@ -192,10 +195,13 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
transport_per_item:
|
transport_per_item:
|
||||||
typeof formItem.transport_per_item === 'string'
|
typeof formItem.transport_per_item === 'string'
|
||||||
? parseFloat(formItem.transport_per_item) || 0
|
? parseFloat(formItem.transport_per_item) || 0
|
||||||
: formItem.transport_per_item || 0,
|
: formItem.transport_per_item || null,
|
||||||
};
|
};
|
||||||
}) || [],
|
}) || [],
|
||||||
travel_documents: values.travel_documents || [],
|
travel_documents:
|
||||||
|
values.travel_documents
|
||||||
|
?.filter((file): file is File => file instanceof File)
|
||||||
|
.filter(Boolean) || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -403,22 +409,13 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
Dokumen Surat Jalan
|
Dokumen Surat Jalan
|
||||||
<span className='text-error'>*</span>
|
<span className='text-error'>*</span>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>Nomor Kendaraan</th>
|
||||||
Nomor Kendaraan
|
<th>Vendor Ekspedisi</th>
|
||||||
<span className='text-error'>*</span>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
Vendor Ekspedisi
|
|
||||||
<span className='text-error'>*</span>
|
|
||||||
</th>
|
|
||||||
<th>
|
<th>
|
||||||
Jumlah Diterima
|
Jumlah Diterima
|
||||||
<span className='text-error'>*</span>
|
<span className='text-error'>*</span>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>Transport/Item</th>
|
||||||
Transport/Item
|
|
||||||
<span className='text-error'>*</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -536,7 +533,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
|
||||||
name={`items.${idx}.vehicle_number`}
|
name={`items.${idx}.vehicle_number`}
|
||||||
type='text'
|
type='text'
|
||||||
value={formItem?.vehicle_number || ''}
|
value={formItem?.vehicle_number || ''}
|
||||||
@@ -562,7 +558,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
value={formItem?.expedition_vendor}
|
value={formItem?.expedition_vendor}
|
||||||
key={`expedition-vendor-${idx}`}
|
key={`expedition-vendor-${idx}`}
|
||||||
@@ -570,6 +565,8 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
expeditionVendorChangeHandler(idx, val)
|
expeditionVendorChangeHandler(idx, val)
|
||||||
}
|
}
|
||||||
options={getExpeditionVendorOptions()}
|
options={getExpeditionVendorOptions()}
|
||||||
|
isLoading={isLoadingExpeditions}
|
||||||
|
onMenuScrollToBottom={loadMoreExpeditions}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(idx, 'expedition_vendor_id')
|
isRepeaterInputError(idx, 'expedition_vendor_id')
|
||||||
.isError
|
.isError
|
||||||
@@ -629,7 +626,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
name={`items.${idx}.transport_per_item`}
|
name={`items.${idx}.transport_per_item`}
|
||||||
value={formItem?.transport_per_item || ''}
|
value={formItem?.transport_per_item || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -680,7 +676,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
|
|
||||||
<div className={'col-span-2 my-2'}>
|
<div className={'col-span-2 my-2'}>
|
||||||
<FileInput
|
<FileInput
|
||||||
required
|
|
||||||
name='travel_documents'
|
name='travel_documents'
|
||||||
label='Dokumen Surat Jalan'
|
label='Dokumen Surat Jalan'
|
||||||
accept='.pdf,.jpg,.jpeg,.png'
|
accept='.pdf,.jpg,.jpeg,.png'
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
|
|||||||
purchase_item_id: number;
|
purchase_item_id: number;
|
||||||
received_date: string;
|
received_date: string;
|
||||||
travel_number: string;
|
travel_number: string;
|
||||||
vehicle_number: string;
|
vehicle_number?: string | null;
|
||||||
expedition_vendor?: {
|
expedition_vendor?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
expedition_vendor_id: number;
|
expedition_vendor_id?: number | null;
|
||||||
received_qty: number | string;
|
received_qty: number | string;
|
||||||
transport_per_item: number | string;
|
transport_per_item?: number | string | null;
|
||||||
}[];
|
}[];
|
||||||
travel_documents: File[];
|
travel_documents?: (File | null | undefined)[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PurchaseStaffApprovalItemSchema = {
|
export type PurchaseStaffApprovalItemSchema = {
|
||||||
@@ -75,14 +75,14 @@ export type PurchaseAcceptApprovalItemSchema = {
|
|||||||
purchase_item_id: number;
|
purchase_item_id: number;
|
||||||
received_date: string;
|
received_date: string;
|
||||||
travel_number: string;
|
travel_number: string;
|
||||||
vehicle_number: string;
|
vehicle_number?: string | null;
|
||||||
expedition_vendor?: {
|
expedition_vendor?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
expedition_vendor_id: number;
|
expedition_vendor_id?: number | null;
|
||||||
received_qty: number | string;
|
received_qty: number | string;
|
||||||
transport_per_item: number | string;
|
transport_per_item?: number | string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PurchaseDeleteItemsSchema = {
|
export type PurchaseDeleteItemsSchema = {
|
||||||
@@ -184,24 +184,19 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
|||||||
.required('No. Surat jalan wajib diisi!')
|
.required('No. Surat jalan wajib diisi!')
|
||||||
.typeError('No. Surat jalan wajib diisi!'),
|
.typeError('No. Surat jalan wajib diisi!'),
|
||||||
vehicle_number: Yup.string()
|
vehicle_number: Yup.string()
|
||||||
.required('Nomor kendaraan wajib diisi!')
|
.nullable()
|
||||||
.typeError('Nomor kendaraan wajib diisi!'),
|
.optional()
|
||||||
|
.typeError('Nomor kendaraan harus berupa plat nomor!'),
|
||||||
expedition_vendor: Yup.object({
|
expedition_vendor: 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(),
|
||||||
expedition_vendor_id: Yup.number()
|
expedition_vendor_id: Yup.number()
|
||||||
.min(1, 'Vendor ekspedisi wajib diisi!')
|
.nullable()
|
||||||
.required('Vendor ekspedisi wajib diisi!')
|
.optional()
|
||||||
.test(
|
.typeError('Vendor ekspedisi harus berupa angka!'),
|
||||||
'is-valid-expedition-vendor',
|
|
||||||
'Vendor ekspedisi harus dipilih!',
|
|
||||||
function (value) {
|
|
||||||
if (!this.parent.expedition_vendor) return true;
|
|
||||||
return Boolean(value && value > 0);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.typeError('Vendor ekspedisi harus dipilih!'),
|
|
||||||
received_qty: Yup.mixed<string | number>()
|
received_qty: Yup.mixed<string | number>()
|
||||||
.required('Jumlah diterima wajib diisi!')
|
.required('Jumlah diterima wajib diisi!')
|
||||||
.test(
|
.test(
|
||||||
@@ -217,13 +212,14 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
|||||||
)
|
)
|
||||||
.typeError('Jumlah diterima harus berupa angka!'),
|
.typeError('Jumlah diterima harus berupa angka!'),
|
||||||
transport_per_item: Yup.mixed<string | number>()
|
transport_per_item: Yup.mixed<string | number>()
|
||||||
.required('Biaya transport per item wajib diisi!')
|
.nullable()
|
||||||
|
.optional()
|
||||||
.test(
|
.test(
|
||||||
'is-valid-transport-per-item',
|
'is-valid-transport-per-item',
|
||||||
'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!',
|
'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!',
|
||||||
function (value) {
|
function (value) {
|
||||||
if (value === '' || value === null || value === undefined)
|
if (value === '' || value === null || value === undefined)
|
||||||
return false;
|
return true;
|
||||||
const numValue =
|
const numValue =
|
||||||
typeof value === 'string' ? parseFloat(value) : value;
|
typeof value === 'string' ? parseFloat(value) : value;
|
||||||
return !isNaN(numValue) && numValue >= 0;
|
return !isNaN(numValue) && numValue >= 0;
|
||||||
@@ -389,16 +385,17 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseR
|
|||||||
travel_documents: Yup.array()
|
travel_documents: Yup.array()
|
||||||
.of(
|
.of(
|
||||||
Yup.mixed<File>()
|
Yup.mixed<File>()
|
||||||
.required('Dokumen surat jalan wajib diupload!')
|
.nullable()
|
||||||
|
.optional()
|
||||||
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
|
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
|
||||||
if (!value) return true;
|
if (!value) return true;
|
||||||
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.required('Dokumen surat jalan wajib diupload!')
|
.nullable()
|
||||||
.min(1, 'Minimal upload 1 dokumen surat jalan!')
|
.optional()
|
||||||
.typeError('Dokumen surat jalan wajib diupload!'),
|
.typeError('Dokumen surat jalan harus berupa array!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
|
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
|
||||||
|
|||||||
@@ -633,8 +633,18 @@ const PurchaseOrderStaffApprovalForm = ({
|
|||||||
|
|
||||||
formik.setFieldValue(`items.${idx}.qty`, numValue);
|
formik.setFieldValue(`items.${idx}.qty`, numValue);
|
||||||
|
|
||||||
formik.setFieldValue(`items.${idx}.price`, '');
|
if (
|
||||||
formik.setFieldValue(`items.${idx}.total_price`, '');
|
formItem.price !== '' &&
|
||||||
|
formItem.price !== undefined &&
|
||||||
|
formItem.price !== null &&
|
||||||
|
numValue !== '' &&
|
||||||
|
numValue > 0
|
||||||
|
) {
|
||||||
|
const calculatedTotal = Number(formItem.price) * Number(numValue);
|
||||||
|
formik.setFieldValue(`items.${idx}.total_price`, calculatedTotal);
|
||||||
|
} else if (numValue === '') {
|
||||||
|
formik.setFieldValue(`items.${idx}.total_price`, '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field === 'price' || field === 'total_price') {
|
if (field === 'price' || field === 'total_price') {
|
||||||
@@ -1184,8 +1194,10 @@ const PurchaseOrderStaffApprovalForm = ({
|
|||||||
color='warning'
|
color='warning'
|
||||||
className='px-4'
|
className='px-4'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
formik.setValues(formikInitialValues);
|
if (type === 'add') {
|
||||||
formik.resetForm();
|
formik.setValues(formikInitialValues);
|
||||||
|
formik.resetForm();
|
||||||
|
}
|
||||||
setPurchaseOrderFormErrorMessage('');
|
setPurchaseOrderFormErrorMessage('');
|
||||||
onCancel?.();
|
onCancel?.();
|
||||||
onModalClose?.();
|
onModalClose?.();
|
||||||
|
|||||||
@@ -63,11 +63,9 @@ const PurchaseRequestForm = ({
|
|||||||
useState('');
|
useState('');
|
||||||
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
||||||
|
|
||||||
// ===== TYPE DEFINITIONS =====
|
const [selectedArea, setSelectedArea] = useState('');
|
||||||
interface ProductOptionType {
|
const [selectedLocation, setSelectedLocation] = useState('');
|
||||||
value: number;
|
const [disabledLocation, setDisabledLocation] = useState(true);
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== UTILITY FUNCTIONS =====
|
// ===== UTILITY FUNCTIONS =====
|
||||||
const isRepeaterInputError = (
|
const isRepeaterInputError = (
|
||||||
@@ -160,11 +158,35 @@ const PurchaseRequestForm = ({
|
|||||||
isLoadingOptions: isLoadingAreas,
|
isLoadingOptions: isLoadingAreas,
|
||||||
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
|
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocations,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
|
hasMore: hasMoreLocations,
|
||||||
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
|
area_id:
|
||||||
|
selectedArea != ''
|
||||||
|
? selectedArea
|
||||||
|
: ((initialValues?.area?.id ?? '') as string),
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
inputValue: warehouseSelectInputValue,
|
inputValue: warehouseSelectInputValue,
|
||||||
setInputValue: setWarehouseSelectInputValue,
|
setInputValue: setWarehouseSelectInputValue,
|
||||||
|
options: warehouseOptions,
|
||||||
isLoadingOptions: isLoadingWarehouses,
|
isLoadingOptions: isLoadingWarehouses,
|
||||||
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
|
loadMore: loadMoreWarehouses,
|
||||||
|
hasMore: hasMoreWarehouses,
|
||||||
|
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search', {
|
||||||
|
area_id:
|
||||||
|
selectedArea != ''
|
||||||
|
? selectedArea
|
||||||
|
: ((initialValues?.area?.id ?? '') as string),
|
||||||
|
location_id:
|
||||||
|
selectedLocation != ''
|
||||||
|
? selectedLocation
|
||||||
|
: ((initialValues?.location?.id ?? '') as string),
|
||||||
|
});
|
||||||
|
|
||||||
// ===== FORM CONFIGURATION =====
|
// ===== FORM CONFIGURATION =====
|
||||||
const formikInitialValues = useMemo<PurchaseRequestFormValues>(
|
const formikInitialValues = useMemo<PurchaseRequestFormValues>(
|
||||||
@@ -267,70 +289,6 @@ const PurchaseRequestForm = ({
|
|||||||
return data;
|
return data;
|
||||||
}, [supplierData]);
|
}, [supplierData]);
|
||||||
|
|
||||||
const locationsUrl = useMemo(() => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
search: locationSelectInputValue,
|
|
||||||
...(formik.values.area_id && formik.values.area_id > 0
|
|
||||||
? { area_id: formik.values.area_id.toString() }
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
return `${LocationApi.basePath}?${params.toString()}`;
|
|
||||||
}, [locationSelectInputValue, formik.values.area_id]);
|
|
||||||
|
|
||||||
const { data: locations, isLoading: isLoadingLocations } = useSWR(
|
|
||||||
locationsUrl,
|
|
||||||
LocationApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const locationOptions = useMemo(() => {
|
|
||||||
if (!isResponseSuccess(locations)) return [];
|
|
||||||
return (
|
|
||||||
locations?.data.map((location) => ({
|
|
||||||
value: location.id,
|
|
||||||
label: location.name,
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}, [locations]);
|
|
||||||
|
|
||||||
const warehousesUrl = useMemo(() => {
|
|
||||||
const params = new URLSearchParams({ search: warehouseSelectInputValue });
|
|
||||||
|
|
||||||
if (formik.values.area_id && formik.values.area_id > 0) {
|
|
||||||
params.append('area_id', formik.values.area_id.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formik.values.location_id && formik.values.location_id > 0) {
|
|
||||||
params.append('location_id', formik.values.location_id.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${WarehouseApi.basePath}?${params.toString()}`;
|
|
||||||
}, [
|
|
||||||
warehouseSelectInputValue,
|
|
||||||
formik.values.area_id,
|
|
||||||
formik.values.location_id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { data: warehouses } = useSWR(
|
|
||||||
warehousesUrl,
|
|
||||||
WarehouseApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const warehouseOptions = useMemo(() => {
|
|
||||||
if (!isResponseSuccess(warehouses)) return [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
warehouses?.data.map((w) => ({
|
|
||||||
value: w.id,
|
|
||||||
label: w.name,
|
|
||||||
area: w.area?.name,
|
|
||||||
location:
|
|
||||||
'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
|
|
||||||
? w.location?.name
|
|
||||||
: undefined,
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}, [warehouses]);
|
|
||||||
|
|
||||||
const addPurchaseItem = () => {
|
const addPurchaseItem = () => {
|
||||||
const newItems = [
|
const newItems = [
|
||||||
...(formik.values.items || []),
|
...(formik.values.items || []),
|
||||||
@@ -407,6 +365,18 @@ const PurchaseRequestForm = ({
|
|||||||
}
|
}
|
||||||
}, [formik.values.supplier_id]);
|
}, [formik.values.supplier_id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (type !== 'add' && initialValues) {
|
||||||
|
if (initialValues.area?.id) {
|
||||||
|
setSelectedArea(initialValues.area.id.toString());
|
||||||
|
setDisabledLocation(false);
|
||||||
|
}
|
||||||
|
if (initialValues.location?.id) {
|
||||||
|
setSelectedLocation(initialValues.location.id.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [type, initialValues]);
|
||||||
|
|
||||||
// ===== FORM HANDLERS =====
|
// ===== FORM HANDLERS =====
|
||||||
const handleSupplierChange = useCallback(
|
const handleSupplierChange = useCallback(
|
||||||
(val: OptionType | OptionType[] | null) => {
|
(val: OptionType | OptionType[] | null) => {
|
||||||
@@ -445,6 +415,16 @@ const PurchaseRequestForm = ({
|
|||||||
formik.setFieldValue('area_id', (area as OptionType)?.value || 0);
|
formik.setFieldValue('area_id', (area as OptionType)?.value || 0);
|
||||||
formik.setFieldTouched('area', true);
|
formik.setFieldTouched('area', true);
|
||||||
formik.setFieldValue('area', area);
|
formik.setFieldValue('area', area);
|
||||||
|
|
||||||
|
setSelectedArea((area as OptionType)?.value as string);
|
||||||
|
setSelectedLocation('');
|
||||||
|
const disabled = (area as OptionType)?.value == null;
|
||||||
|
setDisabledLocation(disabled);
|
||||||
|
|
||||||
|
formik.setFieldTouched('location_id', false);
|
||||||
|
formik.setFieldValue('location_id', 0);
|
||||||
|
formik.setFieldTouched('location', false);
|
||||||
|
formik.setFieldValue('location', null);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -456,6 +436,8 @@ const PurchaseRequestForm = ({
|
|||||||
formik.setFieldValue('location_id', (location as OptionType)?.value || 0);
|
formik.setFieldValue('location_id', (location as OptionType)?.value || 0);
|
||||||
formik.setFieldTouched('location', true);
|
formik.setFieldTouched('location', true);
|
||||||
formik.setFieldValue('location', location);
|
formik.setFieldValue('location', location);
|
||||||
|
|
||||||
|
setSelectedLocation((location as OptionType)?.value as string);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -596,10 +578,15 @@ const PurchaseRequestForm = ({
|
|||||||
placeholder='Pilih Lokasi...'
|
placeholder='Pilih Lokasi...'
|
||||||
value={formik.values.location}
|
value={formik.values.location}
|
||||||
onChange={handleLocationChange}
|
onChange={handleLocationChange}
|
||||||
options={locationOptions}
|
options={
|
||||||
|
selectedArea != '' || initialValues?.area?.id
|
||||||
|
? locationOptions
|
||||||
|
: []
|
||||||
|
}
|
||||||
onInputChange={setLocationSelectInputValue}
|
onInputChange={setLocationSelectInputValue}
|
||||||
isLoading={isLoadingLocations}
|
isLoading={isLoadingLocations}
|
||||||
isDisabled={type === 'detail'}
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
|
isDisabled={type === 'detail' || disabledLocation}
|
||||||
isClearable={type !== 'detail'}
|
isClearable={type !== 'detail'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -713,6 +700,7 @@ const PurchaseRequestForm = ({
|
|||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
onInputChange={setWarehouseSelectInputValue}
|
onInputChange={setWarehouseSelectInputValue}
|
||||||
isLoading={isLoadingWarehouses}
|
isLoading={isLoadingWarehouses}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(idx, 'warehouse_id').isError
|
isRepeaterInputError(idx, 'warehouse_id').isError
|
||||||
}
|
}
|
||||||
@@ -732,9 +720,9 @@ const PurchaseRequestForm = ({
|
|||||||
required
|
required
|
||||||
value={item.product ?? undefined}
|
value={item.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
const product = val as ProductOptionType | null;
|
const product = val as OptionType | null;
|
||||||
const productId =
|
const productId =
|
||||||
(product as ProductOptionType)?.value || 0;
|
(product as OptionType)?.value || 0;
|
||||||
|
|
||||||
formik.setFieldTouched(
|
formik.setFieldTouched(
|
||||||
`items.${idx}.product`,
|
`items.${idx}.product`,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
setInputValue: setAreaInputValue,
|
setInputValue: setAreaInputValue,
|
||||||
options: areaOptions,
|
options: areaOptions,
|
||||||
isLoadingOptions: isLoadingAreaOptions,
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
|
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -101,6 +102,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -118,6 +120,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
setInputValue: setWarehouseInputValue,
|
setInputValue: setWarehouseInputValue,
|
||||||
options: warehouseOptions,
|
options: warehouseOptions,
|
||||||
isLoadingOptions: isLoadingWarehouseOptions,
|
isLoadingOptions: isLoadingWarehouseOptions,
|
||||||
|
loadMore: loadMoreWarehouses,
|
||||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -135,6 +138,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
setInputValue: setCustomerInputValue,
|
setInputValue: setCustomerInputValue,
|
||||||
options: customerOptions,
|
options: customerOptions,
|
||||||
isLoadingOptions: isLoadingCustomerOptions,
|
isLoadingOptions: isLoadingCustomerOptions,
|
||||||
|
loadMore: loadMoreCustomers,
|
||||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -298,6 +302,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
value={selectedArea}
|
value={selectedArea}
|
||||||
onChange={areaChangeHandler}
|
onChange={areaChangeHandler}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
@@ -312,6 +317,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
@@ -326,6 +332,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
value={selectedWarehouse}
|
value={selectedWarehouse}
|
||||||
onChange={warehouseChangeHandler}
|
onChange={warehouseChangeHandler}
|
||||||
onInputChange={setWarehouseInputValue}
|
onInputChange={setWarehouseInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
@@ -340,6 +347,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
value={selectedCustomer}
|
value={selectedCustomer}
|
||||||
onChange={customerChangeHandler}
|
onChange={customerChangeHandler}
|
||||||
onInputChange={setCustomerInputValue}
|
onInputChange={setCustomerInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreCustomers}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ const DailyMarketingsTable = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log({ sorting });
|
// console.log({ sorting });
|
||||||
|
|
||||||
if (sorting.length === 1) {
|
if (sorting.length === 1) {
|
||||||
onFilterByChange(sorting[0].id);
|
onFilterByChange(sorting[0].id);
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ import MenuItem from '@/components/menu/MenuItem';
|
|||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { generateReportExpensePDF } from './pdf/ReportExpenseExport';
|
import { generateReportExpensePDF } from './pdf/ReportExpenseExport';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import {
|
||||||
|
KandangApi,
|
||||||
|
LocationApi,
|
||||||
|
NonstockApi,
|
||||||
|
SupplierApi,
|
||||||
|
} from '@/services/api/master-data';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
|
|
||||||
const ReportExpenseTable = () => {
|
const ReportExpenseTable = () => {
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
@@ -64,16 +73,33 @@ const ReportExpenseTable = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===== SELECT OPTIONS =====
|
// ===== SELECT OPTIONS =====
|
||||||
const { options: optionsLocation, isLoadingOptions: isLoadingLocation } =
|
const {
|
||||||
useSelect(`/master-data/locations`, 'id', 'name');
|
setInputValue: setLocationInputValue,
|
||||||
const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } =
|
options: locationOptions,
|
||||||
useSelect(`/master-data/suppliers`, 'id', 'name');
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
const { options: optionsKandang, isLoadingOptions: isLoadingKandang } =
|
loadMore: loadMoreLocations,
|
||||||
useSelect(`/master-data/kandangs`, 'id', 'name', '', {
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
location_id: filterState.location_id,
|
|
||||||
});
|
const {
|
||||||
const { options: optionsNonstock, isLoadingOptions: isLoadingNonstock } =
|
setInputValue: setSupplierInputValue,
|
||||||
useSelect(`/master-data/nonstocks`, 'id', 'name');
|
options: supplierOptions,
|
||||||
|
isLoadingOptions: isLoadingSupplierOptions,
|
||||||
|
loadMore: loadMoreSuppliers,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setKandangInputValue,
|
||||||
|
options: kandangOptions,
|
||||||
|
isLoadingOptions: isLoadingKandangOptions,
|
||||||
|
loadMore: loadMoreKandangs,
|
||||||
|
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setNonstockInputValue,
|
||||||
|
options: nonstockOptions,
|
||||||
|
isLoadingOptions: isLoadingNonstockOptions,
|
||||||
|
loadMore: loadMoreNonstocks,
|
||||||
|
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const categoryOptions = useMemo(
|
const categoryOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -86,31 +112,31 @@ const ReportExpenseTable = () => {
|
|||||||
// Mendapatkan value option select dari filter state
|
// Mendapatkan value option select dari filter state
|
||||||
const selectedLocation = useMemo(
|
const selectedLocation = useMemo(
|
||||||
() =>
|
() =>
|
||||||
optionsLocation.find(
|
locationOptions.find(
|
||||||
(opt) => String(opt.value) === filterState.location_id
|
(opt) => String(opt.value) === filterState.location_id
|
||||||
) || null,
|
) || null,
|
||||||
[optionsLocation, filterState.location_id]
|
[locationOptions, filterState.location_id]
|
||||||
);
|
);
|
||||||
const selectedSupplier = useMemo(
|
const selectedSupplier = useMemo(
|
||||||
() =>
|
() =>
|
||||||
optionsSupplier.find(
|
supplierOptions.find(
|
||||||
(opt) => String(opt.value) === filterState.supplier_id
|
(opt) => String(opt.value) === filterState.supplier_id
|
||||||
) || null,
|
) || null,
|
||||||
[optionsSupplier, filterState.supplier_id]
|
[supplierOptions, filterState.supplier_id]
|
||||||
);
|
);
|
||||||
const selectedKandang = useMemo(
|
const selectedKandang = useMemo(
|
||||||
() =>
|
() =>
|
||||||
optionsKandang.find(
|
kandangOptions.find(
|
||||||
(opt) => String(opt.value) === filterState.kandang_id
|
(opt) => String(opt.value) === filterState.kandang_id
|
||||||
) || null,
|
) || null,
|
||||||
[optionsKandang, filterState.kandang_id]
|
[kandangOptions, filterState.kandang_id]
|
||||||
);
|
);
|
||||||
const selectedNonstock = useMemo(
|
const selectedNonstock = useMemo(
|
||||||
() =>
|
() =>
|
||||||
optionsNonstock.find(
|
nonstockOptions.find(
|
||||||
(opt) => String(opt.value) === filterState.nonstock_id
|
(opt) => String(opt.value) === filterState.nonstock_id
|
||||||
) || null,
|
) || null,
|
||||||
[optionsNonstock, filterState.nonstock_id]
|
[nonstockOptions, filterState.nonstock_id]
|
||||||
);
|
);
|
||||||
const selectedCategory = useMemo(
|
const selectedCategory = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -756,38 +782,46 @@ const ReportExpenseTable = () => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
options={optionsLocation}
|
options={locationOptions}
|
||||||
isLoading={isLoadingLocation}
|
isLoading={isLoadingLocationOptions}
|
||||||
placeholder='Lokasi'
|
placeholder='Lokasi'
|
||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
label='Kandang'
|
label='Kandang'
|
||||||
options={optionsKandang}
|
options={kandangOptions}
|
||||||
isLoading={isLoadingKandang}
|
isLoading={isLoadingKandangOptions}
|
||||||
placeholder='Kandang'
|
placeholder='Kandang'
|
||||||
value={selectedKandang}
|
value={selectedKandang}
|
||||||
onChange={kandangChangeHandler}
|
onChange={kandangChangeHandler}
|
||||||
|
onInputChange={setKandangInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandangs}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
label='Supplier'
|
label='Supplier'
|
||||||
options={optionsSupplier}
|
options={supplierOptions}
|
||||||
isLoading={isLoadingSupplier}
|
isLoading={isLoadingSupplierOptions}
|
||||||
placeholder='Supplier'
|
placeholder='Supplier'
|
||||||
value={selectedSupplier}
|
value={selectedSupplier}
|
||||||
onChange={supplierChangeHandler}
|
onChange={supplierChangeHandler}
|
||||||
|
onInputChange={setSupplierInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
label='Produk'
|
label='Produk'
|
||||||
options={optionsNonstock}
|
options={nonstockOptions}
|
||||||
isLoading={isLoadingNonstock}
|
isLoading={isLoadingNonstockOptions}
|
||||||
placeholder='Produk'
|
placeholder='Produk'
|
||||||
value={selectedNonstock}
|
value={selectedNonstock}
|
||||||
onChange={nonstockChangeHandler}
|
onChange={nonstockChangeHandler}
|
||||||
|
onInputChange={setNonstockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreNonstocks}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
|
|||||||
@@ -177,10 +177,12 @@ interface CustomerPaymentExportPDFParams {
|
|||||||
data: CustomerPaymentReport[];
|
data: CustomerPaymentReport[];
|
||||||
params?: {
|
params?: {
|
||||||
customer_name?: string;
|
customer_name?: string;
|
||||||
sales?: string;
|
// TODO: Uncomment when BE is ready
|
||||||
|
// sales?: string;
|
||||||
start_date?: string;
|
start_date?: string;
|
||||||
end_date?: string;
|
end_date?: string;
|
||||||
filter_by?: string;
|
// TODO: Uncomment when BE is ready
|
||||||
|
// filter_by?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,9 +197,10 @@ const getParameterText = (
|
|||||||
paramsText.push('Semua Customer');
|
paramsText.push('Semua Customer');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params?.sales) {
|
// TODO: Uncomment when BE is ready
|
||||||
paramsText.push(`Sales: ${params.sales}`);
|
// if (params?.sales) {
|
||||||
}
|
// paramsText.push(`Sales: ${params.sales}`);
|
||||||
|
// }
|
||||||
|
|
||||||
if (params?.start_date && params?.end_date) {
|
if (params?.start_date && params?.end_date) {
|
||||||
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
|
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||||
@@ -242,9 +245,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={pdfStyles.parameterBadge}>
|
{/* TODO: Uncomment when BE is ready */}
|
||||||
|
{/* <View style={pdfStyles.parameterBadge}>
|
||||||
<Text>Filter Tanggal: Tanggal DO</Text>
|
<Text>Filter Tanggal: Tanggal DO</Text>
|
||||||
</View>
|
</View> */}
|
||||||
<View style={pdfStyles.parameterBadge}>
|
<View style={pdfStyles.parameterBadge}>
|
||||||
<Text>
|
<Text>
|
||||||
Customer: {params.params?.customer_name || 'Semua Customer'}
|
Customer: {params.params?.customer_name || 'Semua Customer'}
|
||||||
@@ -280,7 +284,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 0.8 }]}>
|
||||||
<Text>Aging</Text>
|
<Text>Aging</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
<Text>Referensi</Text>
|
<Text>Referensi</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
@@ -296,17 +300,11 @@ 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 style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
|
||||||
<Text>CN</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>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
|
||||||
<Text>Pajak</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>Total</Text>
|
<Text>Total</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -343,13 +341,15 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
||||||
<Text>
|
<Text>
|
||||||
{item.do_date ? formatDate(item.do_date, 'DD MMM YY') : '-'}
|
{item.trans_date
|
||||||
|
? formatDate(item.trans_date, 'DD MMM YY')
|
||||||
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
||||||
<Text>
|
<Text>
|
||||||
{item.realization_date
|
{item.delivery_date
|
||||||
? formatDate(item.realization_date, 'DD MMM YY')
|
? formatDate(item.delivery_date, 'DD MMM YY')
|
||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -358,11 +358,15 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
{item.aging_day ? formatNumber(item.aging_day) : '-'} hari
|
{item.aging_day ? formatNumber(item.aging_day) : '-'} hari
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
<Text>{item.reference || '-'}</Text>
|
<Text>{item.reference || '-'}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
<Text>{item.vehicle_plate || '-'}</Text>
|
<Text>
|
||||||
|
{Array.isArray(item.vehicle_numbers)
|
||||||
|
? item.vehicle_numbers.join(', ')
|
||||||
|
: item.vehicle_numbers || '-'}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
<Text>{formatNumber(item.qty)}</Text>
|
<Text>{formatNumber(item.qty)}</Text>
|
||||||
@@ -374,22 +378,16 @@ 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 style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
|
||||||
<Text>{formatCurrency(item.credit_note)}</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>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>{formatNumber(item.ppn)}%</Text>
|
<Text>{formatCurrency(item.total_price)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>{formatCurrency(item.total)}</Text>
|
<Text>{formatCurrency(item.payment_amount)}</Text>
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
|
||||||
<Text>{formatCurrency(item.payment)}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text style={pdfStyles.textError}>
|
<Text style={pdfStyles.textError}>
|
||||||
@@ -397,30 +395,32 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
{item.notes ? (
|
{item.status ? (
|
||||||
<Text>{item.notes}</Text>
|
|
||||||
) : (
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
pdfStyles.badge,
|
pdfStyles.badge,
|
||||||
item.accounts_receivable === 0
|
item.status === 'LUNAS'
|
||||||
? pdfStyles.badgeLunas
|
? pdfStyles.badgeLunas
|
||||||
: pdfStyles.badgeBelumLunas,
|
: pdfStyles.badgeBelumLunas,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
{item.accounts_receivable === 0
|
{item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'}
|
||||||
? 'Lunas'
|
|
||||||
: 'Belum Lunas'}
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
) : (
|
||||||
|
<Text>-</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
<Text>{item.pickup_info || '-'}</Text>
|
<Text>
|
||||||
|
{Array.isArray(item.pickup_info)
|
||||||
|
? item.pickup_info.join(', ')
|
||||||
|
: item.pickup_info || '-'}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
<Text>{item.sales_marketing || '-'}</Text>
|
<Text>{item.sales_person || '-'}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@@ -440,7 +440,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<View style={[pdfStyles.tableCell, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 0.8 }]}>
|
||||||
<Text></Text>
|
<Text></Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
<Text></Text>
|
<Text></Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
@@ -458,25 +458,13 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<Text></Text>
|
<Text></Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>
|
<Text></Text>
|
||||||
{formatCurrency(
|
|
||||||
customerReport.summary.total_initial_amount
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
|
||||||
<Text>
|
|
||||||
{formatCurrency(customerReport.summary.total_credit_note)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>
|
<Text>
|
||||||
{formatCurrency(customerReport.summary.total_final_amount)}
|
{formatCurrency(customerReport.summary.total_final_amount)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
|
||||||
<Text></Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>
|
<Text>
|
||||||
{formatCurrency(customerReport.summary.total_grand_amount)}
|
{formatCurrency(customerReport.summary.total_grand_amount)}
|
||||||
|
|||||||
@@ -24,30 +24,30 @@ export const generateCustomerPaymentExcel = (
|
|||||||
const excelData: { [key: string]: string | number }[] = customerData.map(
|
const excelData: { [key: string]: string | number }[] = customerData.map(
|
||||||
(item, index) => ({
|
(item, index) => ({
|
||||||
No: index + 1,
|
No: index + 1,
|
||||||
'Tanggal DO/Bayar': item.do_date
|
'Tanggal DO/Bayar': item.trans_date
|
||||||
? formatDate(item.do_date, 'DD MMM YYYY')
|
? formatDate(item.trans_date, 'DD MMM YYYY')
|
||||||
: '',
|
: '',
|
||||||
'Tanggal Realisasi': item.realization_date
|
'Tanggal Realisasi': item.delivery_date
|
||||||
? formatDate(item.realization_date, 'DD MMM YYYY')
|
? formatDate(item.delivery_date, 'DD MMM YYYY')
|
||||||
: '',
|
: '',
|
||||||
Aging: formatNumber(item.aging_day || 0),
|
Aging: formatNumber(item.aging_day || 0),
|
||||||
Referensi: item.reference || '',
|
Referensi: item.reference || '',
|
||||||
'Nomor Polisi': Array.isArray(item.vehicle_plate)
|
'Nomor Polisi': Array.isArray(item.vehicle_numbers)
|
||||||
? item.vehicle_plate.join(', ')
|
? item.vehicle_numbers.join(', ')
|
||||||
: '',
|
: '',
|
||||||
'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),
|
||||||
CN: formatCurrency(item.credit_note || 0),
|
|
||||||
'Harga Akhir': formatCurrency(item.final_price || 0),
|
'Harga Akhir': formatCurrency(item.final_price || 0),
|
||||||
'PPN (%)': formatNumber(item.ppn || 0),
|
Total: formatCurrency(item.total_price || 0),
|
||||||
Total: formatCurrency(item.total || 0),
|
Pembayaran: formatCurrency(item.payment_amount || 0),
|
||||||
Pembayaran: formatCurrency(item.payment || 0),
|
|
||||||
'Saldo Piutang': formatCurrency(item.accounts_receivable || 0),
|
'Saldo Piutang': formatCurrency(item.accounts_receivable || 0),
|
||||||
Keterangan: item.notes || '',
|
Keterangan: item.status || '',
|
||||||
Pengambilan: item.pickup_info || '',
|
Pengambilan: Array.isArray(item.pickup_info)
|
||||||
'Sales/Marketing': item.sales_marketing || '',
|
? item.pickup_info.join(', ')
|
||||||
|
: '',
|
||||||
|
'Sales/Marketing': item.sales_person || '',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -62,14 +62,10 @@ 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': formatCurrency(
|
'Harga/Unit': '',
|
||||||
customerReport.summary.total_initial_amount || 0
|
|
||||||
),
|
|
||||||
CN: formatCurrency(customerReport.summary.total_credit_note || 0),
|
|
||||||
'Harga Akhir': formatCurrency(
|
'Harga Akhir': formatCurrency(
|
||||||
customerReport.summary.total_final_amount || 0
|
customerReport.summary.total_final_amount || 0
|
||||||
),
|
),
|
||||||
'PPN (%)': '',
|
|
||||||
Total: formatCurrency(customerReport.summary.total_grand_amount || 0),
|
Total: formatCurrency(customerReport.summary.total_grand_amount || 0),
|
||||||
Pembayaran: formatCurrency(customerReport.summary.total_payment || 0),
|
Pembayaran: formatCurrency(customerReport.summary.total_payment || 0),
|
||||||
'Saldo Piutang': formatCurrency(
|
'Saldo Piutang': formatCurrency(
|
||||||
@@ -93,10 +89,8 @@ 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: 10 }, // CN
|
|
||||||
{ wch: 15 }, // Harga Akhir
|
{ wch: 15 }, // Harga Akhir
|
||||||
{ wch: 10 }, // PPN
|
|
||||||
{ wch: 15 }, // Total
|
{ wch: 15 }, // Total
|
||||||
{ wch: 15 }, // Pembayaran
|
{ wch: 15 }, // Pembayaran
|
||||||
{ wch: 15 }, // Saldo Piutang
|
{ wch: 15 }, // Saldo Piutang
|
||||||
|
|||||||
@@ -187,10 +187,30 @@ const pdfStyles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
},
|
},
|
||||||
|
parameterBadge: {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
color: '#333333',
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
marginRight: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
parameterContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface DebtSupplierExportPDFParams {
|
interface DebtSupplierExportPDFParams {
|
||||||
data: DebtSupplier[];
|
data: DebtSupplier[];
|
||||||
|
params?: {
|
||||||
|
supplier_name?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
filter_by?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||||
@@ -208,9 +228,50 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
<Text style={pdfStyles.mainTitle}>
|
<Text style={pdfStyles.mainTitle}>
|
||||||
Laporan > Rekapitulasi Hutang ke Supplier
|
Laporan > Rekapitulasi Hutang ke Supplier
|
||||||
</Text>
|
</Text>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Periode:{' '}
|
||||||
|
{params.params?.start_date
|
||||||
|
? formatDate(params.params.start_date, 'DD MMM YYYY')
|
||||||
|
: '-'}{' '}
|
||||||
|
s.d{' '}
|
||||||
|
{params.params?.end_date
|
||||||
|
? formatDate(params.params.end_date, 'DD MMM YYYY')
|
||||||
|
: '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{params.params?.filter_by && (
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Filter Tanggal:{' '}
|
||||||
|
{params.params.filter_by === 'po_date'
|
||||||
|
? 'Tanggal PO'
|
||||||
|
: params.params.filter_by === 'received_date'
|
||||||
|
? 'Tanggal Terima'
|
||||||
|
: params.params.filter_by === 'due_date'
|
||||||
|
? 'Tanggal Jatuh Tempo'
|
||||||
|
: params.params.filter_by}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
<Text style={pdfStyles.supplierTitle}>
|
<Text style={pdfStyles.supplierTitle}>
|
||||||
{supplierReport.supplier.name}
|
{supplierReport.supplier.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text style={pdfStyles.supplierInfo}>
|
||||||
|
{supplierReport.supplier.category}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
|
|||||||
@@ -94,18 +94,18 @@ export const generateDebtSupplierExcel = (
|
|||||||
|
|
||||||
const colWidths = [
|
const colWidths = [
|
||||||
{ wch: 5 }, // No
|
{ wch: 5 }, // No
|
||||||
{ wch: 15 }, // Nomor PR
|
{ wch: 10 }, // Nomor PR
|
||||||
{ wch: 15 }, // Nomor PO
|
{ wch: 10 }, // Nomor PO
|
||||||
{ wch: 15 }, // Tanggal Terima/Bayar
|
{ wch: 20 }, // Tanggal Terima/Bayar
|
||||||
{ wch: 15 }, // Tanggal PO
|
{ wch: 10 }, // Tanggal PO
|
||||||
{ wch: 12 }, // Aging
|
{ wch: 10 }, // Aging
|
||||||
{ wch: 15 }, // Area
|
{ wch: 15 }, // Area
|
||||||
{ wch: 15 }, // Gudang
|
{ wch: 15 }, // Gudang
|
||||||
{ wch: 18 }, // Jatuh Tempo
|
{ wch: 12 }, // Jatuh Tempo
|
||||||
{ wch: 18 }, // Status Jatuh Tempo
|
{ wch: 20 }, // Status Jatuh Tempo
|
||||||
{ wch: 15 }, // Nominal Pembelian (Rp)
|
{ wch: 20 }, // Nominal Pembelian (Rp)
|
||||||
{ wch: 15 }, // Pembayaran (Rp)
|
{ wch: 15 }, // Pembayaran (Rp)
|
||||||
{ wch: 15 }, // Sisa Saldo Hutang (Rp)
|
{ wch: 20 }, // Sisa Saldo Hutang (Rp)
|
||||||
{ wch: 12 }, // Status
|
{ wch: 12 }, // Status
|
||||||
{ wch: 15 }, // Nomor Perjalanan
|
{ wch: 15 }, // Nomor Perjalanan
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ const CustomerPaymentTab = () => {
|
|||||||
const [filterCustomer, setFilterCustomer] = useState<typeof customerOptions>(
|
const [filterCustomer, setFilterCustomer] = useState<typeof customerOptions>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
// TODO: Uncomment when BE is ready
|
||||||
|
// const [filterSales, setFilterSales] = useState<typeof salesOptions>([]);
|
||||||
const [filterSales, setFilterSales] = useState<typeof salesOptions>([]);
|
const [filterSales, setFilterSales] = useState<typeof salesOptions>([]);
|
||||||
const [filterStartDate, setFilterStartDate] = useState('');
|
const [filterStartDate, setFilterStartDate] = useState('');
|
||||||
const [filterEndDate, setFilterEndDate] = useState('');
|
const [filterEndDate, setFilterEndDate] = useState('');
|
||||||
@@ -55,13 +57,16 @@ const CustomerPaymentTab = () => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
options: customerOptions,
|
options: customerOptions,
|
||||||
|
setInputValue: setCustomerInputValue,
|
||||||
isLoadingOptions: isLoadingCustomers,
|
isLoadingOptions: isLoadingCustomers,
|
||||||
loadMore: loadMoreCustomers,
|
loadMore: loadMoreCustomers,
|
||||||
hasMore: hasMoreCustomers,
|
hasMore: hasMoreCustomers,
|
||||||
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
// TODO: Uncomment when BE is ready
|
||||||
const {
|
const {
|
||||||
options: salesOptions,
|
options: salesOptions,
|
||||||
|
setInputValue: setSalesInputValue,
|
||||||
isLoadingOptions: isLoadingSales,
|
isLoadingOptions: isLoadingSales,
|
||||||
loadMore: loadMoreSales,
|
loadMore: loadMoreSales,
|
||||||
hasMore: hasMoreSales,
|
hasMore: hasMoreSales,
|
||||||
@@ -101,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 =====
|
||||||
@@ -133,23 +142,18 @@ const CustomerPaymentTab = () => {
|
|||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sales filter
|
// TODO: Uncomment when BE is ready
|
||||||
if (filterSales.length > 0) {
|
// // Sales filter
|
||||||
count += 1;
|
// if (filterSales.length > 0) {
|
||||||
}
|
// count += 1;
|
||||||
|
// }
|
||||||
// Filter by (always count if submitted)
|
|
||||||
if (isSubmitted) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}, [
|
}, [
|
||||||
filterStartDate,
|
filterStartDate,
|
||||||
filterEndDate,
|
filterEndDate,
|
||||||
filterCustomer,
|
filterCustomer,
|
||||||
filterSales,
|
// filterSales,
|
||||||
isSubmitted,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const hasFilters = activeFiltersCount > 0;
|
const hasFilters = activeFiltersCount > 0;
|
||||||
@@ -159,15 +163,16 @@ 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,
|
||||||
sales_id:
|
// TODO: Uncomment when BE is ready
|
||||||
filterSales.length > 0
|
// sales_id:
|
||||||
? filterSales.map((v) => String(v.value)).join(',')
|
// filterSales.length > 0
|
||||||
: undefined,
|
// ? filterSales.map((v) => String(v.value)).join(',')
|
||||||
filter_by: 'do_date' as const,
|
// : undefined,
|
||||||
|
// filter_by: 'do_date' as const,
|
||||||
start_date: filterStartDate || undefined,
|
start_date: filterStartDate || undefined,
|
||||||
end_date: filterEndDate || undefined,
|
end_date: filterEndDate || undefined,
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
@@ -179,9 +184,9 @@ const CustomerPaymentTab = () => {
|
|||||||
: null,
|
: null,
|
||||||
([, params]) =>
|
([, params]) =>
|
||||||
FinanceApi.getCustomerPaymentReport(
|
FinanceApi.getCustomerPaymentReport(
|
||||||
params.customer_id,
|
params.customer_ids,
|
||||||
params.sales_id,
|
undefined, // TODO: Change to params.sales_id when BE is ready
|
||||||
params.filter_by,
|
undefined, // TODO: Change to params.filter_by when BE is ready
|
||||||
params.start_date,
|
params.start_date,
|
||||||
params.end_date,
|
params.end_date,
|
||||||
params.page,
|
params.page,
|
||||||
@@ -202,15 +207,15 @@ 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,
|
||||||
sales_id:
|
// TODO: Uncomment when BE is ready
|
||||||
filterSales.length > 0
|
// sales_id:
|
||||||
? filterSales.map((v) => String(v.value)).join(',')
|
// filterSales.length > 0
|
||||||
: undefined,
|
// ? filterSales.map((v) => String(v.value)).join(',')
|
||||||
filter_by: 'do_date' as const,
|
// : undefined,
|
||||||
start_date: filterStartDate || undefined,
|
start_date: filterStartDate || undefined,
|
||||||
end_date: filterEndDate || undefined,
|
end_date: filterEndDate || undefined,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
@@ -218,9 +223,9 @@ const CustomerPaymentTab = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await FinanceApi.getCustomerPaymentReport(
|
const response = await FinanceApi.getCustomerPaymentReport(
|
||||||
params.customer_id,
|
params.customer_ids,
|
||||||
params.sales_id,
|
undefined, // TODO: Change to params.sales_id when BE is ready
|
||||||
params.filter_by,
|
undefined, // TODO: Change to params.filter_by when BE is ready
|
||||||
params.start_date,
|
params.start_date,
|
||||||
params.end_date,
|
params.end_date,
|
||||||
params.page,
|
params.page,
|
||||||
@@ -277,13 +282,15 @@ const CustomerPaymentTab = () => {
|
|||||||
filterCustomer.length > 0
|
filterCustomer.length > 0
|
||||||
? filterCustomer.map((c) => c.label).join(', ')
|
? filterCustomer.map((c) => c.label).join(', ')
|
||||||
: undefined,
|
: undefined,
|
||||||
sales:
|
// TODO: Uncomment when BE is ready
|
||||||
filterSales.length > 0
|
// sales:
|
||||||
? filterSales.map((s) => s.label).join(', ')
|
// filterSales.length > 0
|
||||||
: undefined,
|
// ? filterSales.map((s) => s.label).join(', ')
|
||||||
|
// : undefined,
|
||||||
start_date: filterStartDate || undefined,
|
start_date: filterStartDate || undefined,
|
||||||
end_date: filterEndDate || undefined,
|
end_date: filterEndDate || undefined,
|
||||||
filter_by: 'do_date',
|
// TODO: Uncomment when BE is ready
|
||||||
|
// filter_by: 'do_date' as const,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
@@ -301,36 +308,41 @@ const CustomerPaymentTab = () => {
|
|||||||
{
|
{
|
||||||
id: 'no',
|
id: 'no',
|
||||||
header: 'No',
|
header: 'No',
|
||||||
cell: (props) => props.row.index + 1,
|
cell: (props) => props.row.index,
|
||||||
footer: () => <div className='font-semibold text-gray-900'>Total</div>,
|
footer: () => <div className='font-semibold text-gray-900'>Total</div>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'do_date_or_payment_date',
|
id: 'do_date_or_payment_date',
|
||||||
header: 'Tanggal DO/Bayar',
|
header: 'Tanggal Jual/Bayar',
|
||||||
accessorKey: 'do_date',
|
accessorKey: 'trans_date',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.do_date;
|
const value = props.row.original.trans_date;
|
||||||
return formatDate(value, 'DD MMM YYYY');
|
return value ? formatDate(value, 'DD MMM YYYY') : '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'realization_date',
|
id: 'realization_date',
|
||||||
header: 'Tanggal Realisasi',
|
header: 'Tanggal Realisasi',
|
||||||
accessorKey: 'realization_date',
|
accessorKey: 'delivery_date',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.realization_date;
|
const value = props.row.original.delivery_date;
|
||||||
return formatDate(value, 'DD MMM YYYY');
|
return value ? formatDate(value, 'DD MMM YYYY') : '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'aging',
|
id: 'aging',
|
||||||
header: 'Aging',
|
header: 'Aging',
|
||||||
accessorKey: 'aging_day',
|
accessorKey: 'aging_day',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
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 ? formatNumber(value) : '-'} hari
|
{value !== null && value !== undefined
|
||||||
|
? `${formatNumber(value)} hari`
|
||||||
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -339,6 +351,7 @@ const CustomerPaymentTab = () => {
|
|||||||
id: 'reference',
|
id: 'reference',
|
||||||
header: 'Referensi',
|
header: 'Referensi',
|
||||||
accessorKey: 'reference',
|
accessorKey: 'reference',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.reference;
|
const value = props.row.original.reference;
|
||||||
return value || '-';
|
return value || '-';
|
||||||
@@ -347,16 +360,18 @@ const CustomerPaymentTab = () => {
|
|||||||
{
|
{
|
||||||
id: 'vehicle_plate',
|
id: 'vehicle_plate',
|
||||||
header: 'Nomor Polisi',
|
header: 'Nomor Polisi',
|
||||||
accessorKey: 'vehicle_plate',
|
accessorKey: 'vehicle_numbers',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.vehicle_plate;
|
const value = props.row.original.vehicle_numbers;
|
||||||
return value || '-';
|
return Array.isArray(value) ? value.join(', ') : value || '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'qty',
|
id: 'qty',
|
||||||
header: 'Ekor/Qty',
|
header: 'Qty',
|
||||||
accessorKey: 'qty',
|
accessorKey: 'qty',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.qty;
|
const value = props.row.original.qty;
|
||||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
@@ -371,6 +386,7 @@ const CustomerPaymentTab = () => {
|
|||||||
id: 'weight',
|
id: 'weight',
|
||||||
header: 'Berat (Kg)',
|
header: 'Berat (Kg)',
|
||||||
accessorKey: 'weight',
|
accessorKey: 'weight',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.weight;
|
const value = props.row.original.weight;
|
||||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
@@ -385,6 +401,7 @@ const CustomerPaymentTab = () => {
|
|||||||
id: 'average_weight',
|
id: 'average_weight',
|
||||||
header: 'AVG',
|
header: 'AVG',
|
||||||
accessorKey: 'average_weight',
|
accessorKey: 'average_weight',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.average_weight;
|
const value = props.row.original.average_weight;
|
||||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
@@ -394,37 +411,23 @@ const CustomerPaymentTab = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'price',
|
id: 'unit_price',
|
||||||
header: 'Harga Awal',
|
header: 'Harga/Unit',
|
||||||
accessorKey: 'price',
|
accessorKey: 'unit_price',
|
||||||
|
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: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>-</div>
|
||||||
{formatCurrency(summary.total_initial_amount) || '-'}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'credit_note',
|
|
||||||
header: 'CN',
|
|
||||||
accessorKey: 'credit_note',
|
|
||||||
cell: (props) => {
|
|
||||||
const value = props.row.original.credit_note;
|
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
||||||
},
|
|
||||||
footer: () => (
|
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
|
||||||
{formatCurrency(summary.total_credit_note) || '-'}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'final_price',
|
id: 'final_price',
|
||||||
header: 'Harga Akhir',
|
header: 'Harga Akhir',
|
||||||
accessorKey: 'final_price',
|
accessorKey: 'final_price',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.final_price;
|
const value = props.row.original.final_price;
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
@@ -435,24 +438,13 @@ const CustomerPaymentTab = () => {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'ppn',
|
|
||||||
header: 'PPN (%)',
|
|
||||||
accessorKey: 'ppn',
|
|
||||||
cell: (props) => {
|
|
||||||
const value = props.row.original.ppn;
|
|
||||||
return <div className='text-right'>{formatNumber(value)}%</div>;
|
|
||||||
},
|
|
||||||
footer: () => (
|
|
||||||
<div className='text-right font-semibold text-gray-900'>-</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'total',
|
id: 'total',
|
||||||
header: 'Total',
|
header: 'Total',
|
||||||
accessorKey: 'total',
|
accessorKey: 'total_price',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.total;
|
const value = props.row.original.total_price;
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
@@ -464,9 +456,10 @@ const CustomerPaymentTab = () => {
|
|||||||
{
|
{
|
||||||
id: 'payment',
|
id: 'payment',
|
||||||
header: 'Pembayaran',
|
header: 'Pembayaran',
|
||||||
accessorKey: 'payment',
|
accessorKey: 'payment_amount',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.payment;
|
const value = props.row.original.payment_amount;
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
@@ -479,14 +472,25 @@ const CustomerPaymentTab = () => {
|
|||||||
id: 'accounts_receivable',
|
id: 'accounts_receivable',
|
||||||
header: 'Saldo Piutang',
|
header: 'Saldo Piutang',
|
||||||
accessorKey: 'accounts_receivable',
|
accessorKey: 'accounts_receivable',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.accounts_receivable;
|
const value = props.row.original.accounts_receivable;
|
||||||
return (
|
return (
|
||||||
<div className='text-right text-error'>{formatCurrency(value)}</div>
|
<div
|
||||||
|
className={`text-right font-semibold ${
|
||||||
|
value < 0 ? 'text-error' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formatCurrency(value)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div
|
||||||
|
className={`text-right font-semibold ${
|
||||||
|
summary.total_accounts_receivable < 0 ? 'text-error' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{formatCurrency(summary.total_accounts_receivable) || '-'}
|
{formatCurrency(summary.total_accounts_receivable) || '-'}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -494,9 +498,10 @@ const CustomerPaymentTab = () => {
|
|||||||
{
|
{
|
||||||
id: 'notes',
|
id: 'notes',
|
||||||
header: 'Keterangan',
|
header: 'Keterangan',
|
||||||
accessorKey: 'notes',
|
accessorKey: 'status',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.notes;
|
const value = props.row.original.status;
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '-';
|
return '-';
|
||||||
@@ -511,7 +516,7 @@ const CustomerPaymentTab = () => {
|
|||||||
status: getPaymentStatusIndicatorColor(value),
|
status: getPaymentStatusIndicatorColor(value),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getPaymentStatusText(value)}
|
<span className='capitalize'>{getPaymentStatusText(value)}</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -520,17 +525,19 @@ const CustomerPaymentTab = () => {
|
|||||||
id: 'pickup_info',
|
id: 'pickup_info',
|
||||||
header: 'Pengambilan',
|
header: 'Pengambilan',
|
||||||
accessorKey: 'pickup_info',
|
accessorKey: 'pickup_info',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.pickup_info;
|
const value = props.row.original.pickup_info;
|
||||||
return value || '-';
|
return Array.isArray(value) ? value.join(', ') : value || '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sales_marketing',
|
id: 'sales_marketing',
|
||||||
header: 'Sales/Marketing',
|
header: 'Sales/Marketing',
|
||||||
accessorKey: 'sales_marketing',
|
accessorKey: 'sales_person',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.sales_marketing;
|
const value = props.row.original.sales_person;
|
||||||
return value || '-';
|
return value || '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -654,6 +661,7 @@ const CustomerPaymentTab = () => {
|
|||||||
Array.isArray(val) ? val : val ? [val] : []
|
Array.isArray(val) ? val : val ? [val] : []
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setCustomerInputValue}
|
||||||
isLoading={isLoadingCustomers}
|
isLoading={isLoadingCustomers}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreCustomers}
|
onMenuScrollToBottom={loadMoreCustomers}
|
||||||
@@ -661,7 +669,8 @@ const CustomerPaymentTab = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* TODO: Uncomment when BE is ready */}
|
||||||
|
{/* <div>
|
||||||
<SelectInputCheckbox
|
<SelectInputCheckbox
|
||||||
label='Sales'
|
label='Sales'
|
||||||
placeholder='Pilih Sales'
|
placeholder='Pilih Sales'
|
||||||
@@ -670,14 +679,16 @@ const CustomerPaymentTab = () => {
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
|
setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setSalesInputValue}
|
||||||
isLoading={isLoadingSales}
|
isLoading={isLoadingSales}
|
||||||
isClearable
|
isClearable
|
||||||
onMenuScrollToBottom={loadMoreSales}
|
onMenuScrollToBottom={loadMoreSales}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div>
|
{/* TODO: Uncomment when BE is ready */}
|
||||||
|
{/* <div>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Filter Berdasarkan'
|
label='Filter Berdasarkan'
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
placeholder='Pilih Filter Berdasarkan'
|
||||||
@@ -686,7 +697,7 @@ const CustomerPaymentTab = () => {
|
|||||||
isDisabled={true}
|
isDisabled={true}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
@@ -726,10 +737,7 @@ const CustomerPaymentTab = () => {
|
|||||||
const summary = customerReport.summary || {
|
const summary = customerReport.summary || {
|
||||||
total_qty: 0,
|
total_qty: 0,
|
||||||
total_weight: 0,
|
total_weight: 0,
|
||||||
total_initial_amount: 0,
|
|
||||||
total_credit_note: 0,
|
|
||||||
total_final_amount: 0,
|
total_final_amount: 0,
|
||||||
total_ppn: 0,
|
|
||||||
total_grand_amount: 0,
|
total_grand_amount: 0,
|
||||||
total_payment: 0,
|
total_payment: 0,
|
||||||
total_accounts_receivable: 0,
|
total_accounts_receivable: 0,
|
||||||
@@ -741,19 +749,27 @@ const CustomerPaymentTab = () => {
|
|||||||
<Card
|
<Card
|
||||||
key={customerReport.customer.id}
|
key={customerReport.customer.id}
|
||||||
title={customerReport.customer.name}
|
title={customerReport.customer.name}
|
||||||
|
subtitle={`(${customerReport.customer.address})`}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'w-full rounded-2xl',
|
wrapper: 'w-full rounded-2xl',
|
||||||
body: 'p-0',
|
body: 'p-0',
|
||||||
title:
|
title:
|
||||||
'py-1.5 px-3 bg-[#0069E0] text-white text-lg font-normal',
|
'py-1.5 px-3 bg-[#0069E0] text-white text-lg font-normal',
|
||||||
|
subtitle:
|
||||||
|
'px-3 pb-1 bg-[#0069E0] text-white text-sm font-normal',
|
||||||
}}
|
}}
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
data={customerReport.rows}
|
data={[
|
||||||
|
{
|
||||||
|
accounts_receivable: customerReport.initial_balance,
|
||||||
|
} as CustomerPaymentReport['rows'][0],
|
||||||
|
...customerReport.rows,
|
||||||
|
]}
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
pageSize={10}
|
pageSize={customerReport.rows.length + 1}
|
||||||
renderFooter={customerReport.rows.length > 0}
|
renderFooter={customerReport.rows.length > 0}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full',
|
containerClassName: 'w-full',
|
||||||
@@ -773,6 +789,36 @@ const CustomerPaymentTab = () => {
|
|||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
paginationClassName: 'hidden',
|
paginationClassName: 'hidden',
|
||||||
}}
|
}}
|
||||||
|
renderCustomRow={(row) => {
|
||||||
|
if (row.index === 0) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||||
|
key={row.index}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
||||||
|
colSpan={13}
|
||||||
|
></td>
|
||||||
|
<td className='px-4 py-3 text-xs whitespace-nowrap'>
|
||||||
|
<div
|
||||||
|
className={`text-right ${
|
||||||
|
row.original.accounts_receivable < 0
|
||||||
|
? 'text-error'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{formatCurrency(row.original.accounts_receivable)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
||||||
|
colSpan={4}
|
||||||
|
></td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
import { Color } from '@/types/theme';
|
import { Color } from '@/types/theme';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
|
||||||
const dueStatus: Record<string, Color> = {
|
const dueStatus: Record<string, Color> = {
|
||||||
'Sudah Jatuh Tempo': 'error',
|
'Sudah Jatuh Tempo': 'error',
|
||||||
@@ -89,10 +90,12 @@ const DebtSupplierTab = () => {
|
|||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
|
const {
|
||||||
useSelect(SupplierApi.basePath, 'id', 'name', '', {
|
setInputValue: setSupplierInputValue,
|
||||||
limit: 'limit',
|
options: supplierOptions,
|
||||||
});
|
isLoadingOptions: isLoadingSupplierOptions,
|
||||||
|
loadMore: loadMoreSuppliers,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const dataTypeOptions = useMemo(
|
const dataTypeOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -247,7 +250,17 @@ const DebtSupplierTab = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await generateDebtSupplierPDF({ data: allDataForExport });
|
await generateDebtSupplierPDF({
|
||||||
|
data: allDataForExport,
|
||||||
|
params: {
|
||||||
|
supplier_name: formik.values.supplierIds
|
||||||
|
?.map((v) => v.label)
|
||||||
|
.join(', '),
|
||||||
|
filter_by: formik.values.filterBy?.label,
|
||||||
|
start_date: formik.values.startDate || undefined,
|
||||||
|
end_date: formik.values.endDate || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||||
@@ -670,7 +683,9 @@ const DebtSupplierTab = () => {
|
|||||||
Array.isArray(val) ? val : val ? [val] : null
|
Array.isArray(val) ? val : val ? [val] : null
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
isLoading={isLoadingSuppliers}
|
onInputChange={setSupplierInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isLoading={isLoadingSupplierOptions}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isError={
|
isError={
|
||||||
|
|||||||
@@ -21,10 +21,18 @@ import {
|
|||||||
ProjectFlockApi,
|
ProjectFlockApi,
|
||||||
ProjectFlockKandangApi,
|
ProjectFlockKandangApi,
|
||||||
} from '@/services/api/production';
|
} from '@/services/api/production';
|
||||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
import {
|
||||||
import { isResponseError } from '@/lib/api-helper';
|
BaseProjectFlockKandang,
|
||||||
|
ProjectFlockKandang,
|
||||||
|
} from '@/types/api/production/project-flock-kandang';
|
||||||
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import Pagination from '@/components/Pagination';
|
import Pagination from '@/components/Pagination';
|
||||||
import { ProductionResultReportApi } from '@/services/api/report/production-result';
|
import { ProductionResultReportApi } from '@/services/api/report/production-result';
|
||||||
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
|
import { httpClient } from '@/services/http/client';
|
||||||
|
import { ProductionResult } from '@/types/api/report/production-result';
|
||||||
|
import ProductionResultReportPDF from './ProductionResultReportPDF';
|
||||||
|
import { pdf } from '@react-pdf/renderer';
|
||||||
|
|
||||||
const ProductionResultContent = () => {
|
const ProductionResultContent = () => {
|
||||||
const [projectFlockKandangs, setProjectFlockKandangs] = useState<
|
const [projectFlockKandangs, setProjectFlockKandangs] = useState<
|
||||||
@@ -49,6 +57,8 @@ const ProductionResultContent = () => {
|
|||||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false);
|
||||||
|
|
||||||
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
null
|
null
|
||||||
@@ -62,6 +72,7 @@ const ProductionResultContent = () => {
|
|||||||
setInputValue: setAreaInputValue,
|
setInputValue: setAreaInputValue,
|
||||||
options: areaOptions,
|
options: areaOptions,
|
||||||
isLoadingOptions: isLoadingAreaOptions,
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
} = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name');
|
} = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -78,6 +89,7 @@ const ProductionResultContent = () => {
|
|||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', {
|
} = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '',
|
area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '',
|
||||||
});
|
});
|
||||||
@@ -94,6 +106,7 @@ const ProductionResultContent = () => {
|
|||||||
setInputValue: setProjectFlockInputValue,
|
setInputValue: setProjectFlockInputValue,
|
||||||
options: projectFlockOptions,
|
options: projectFlockOptions,
|
||||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
} = useSelect<BaseKandang>(
|
} = useSelect<BaseKandang>(
|
||||||
ProjectFlockApi.basePath,
|
ProjectFlockApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
@@ -120,6 +133,7 @@ const ProductionResultContent = () => {
|
|||||||
setInputValue: setProjectFlockKandangInputValue,
|
setInputValue: setProjectFlockKandangInputValue,
|
||||||
options: projectFlockKandangOptions,
|
options: projectFlockKandangOptions,
|
||||||
isLoadingOptions: isLoadingProjectFlockKandangOptions,
|
isLoadingOptions: isLoadingProjectFlockKandangOptions,
|
||||||
|
loadMore: loadMoreProjectFlockKandangs,
|
||||||
} = useSelect<BaseKandang>(
|
} = useSelect<BaseKandang>(
|
||||||
ProjectFlockKandangApi.basePath,
|
ProjectFlockKandangApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
@@ -154,6 +168,87 @@ const ProductionResultContent = () => {
|
|||||||
setIsLoadingExportingToExcel(false);
|
setIsLoadingExportingToExcel(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportToPdfHandler = async () => {
|
||||||
|
setIsLoadingExportingToPdf(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let projectFlockKandangsData: BaseProjectFlockKandang[] = [];
|
||||||
|
|
||||||
|
if (selectedProjectFlockKandang) {
|
||||||
|
const projectFlockKandangResponse =
|
||||||
|
await ProjectFlockKandangApi.getSingle(
|
||||||
|
selectedProjectFlockKandang?.value as number
|
||||||
|
);
|
||||||
|
|
||||||
|
projectFlockKandangsData = isResponseSuccess(
|
||||||
|
projectFlockKandangResponse
|
||||||
|
)
|
||||||
|
? [projectFlockKandangResponse.data]
|
||||||
|
: [];
|
||||||
|
} else {
|
||||||
|
const projectFlockKandangsResponse =
|
||||||
|
await ProjectFlockKandangApi.getAll({
|
||||||
|
area_id: selectedArea?.value,
|
||||||
|
project_flock_id: selectedProjectFlock?.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
projectFlockKandangsData = isResponseSuccess(
|
||||||
|
projectFlockKandangsResponse
|
||||||
|
)
|
||||||
|
? projectFlockKandangsResponse.data
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedProductionResults: {
|
||||||
|
projectFlockKandang: BaseProjectFlockKandang;
|
||||||
|
productionResult: ProductionResult[] | null;
|
||||||
|
}[] = await Promise.all(
|
||||||
|
projectFlockKandangsData.map(async (projectFlockKandang) => {
|
||||||
|
const getProductionResultPath = `${ProductionResultReportApi.basePath}/${projectFlockKandang.id}?page=1&limit=100`;
|
||||||
|
const getProductionResultRes = await httpClient<
|
||||||
|
BaseApiResponse<ProductionResult[]>
|
||||||
|
>(getProductionResultPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectFlockKandang,
|
||||||
|
productionResult: isResponseSuccess(getProductionResultRes)
|
||||||
|
? getProductionResultRes.data
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mappedProductionResults.length === 0) {
|
||||||
|
toast.error('Tidak ada data untuk diexport.');
|
||||||
|
setIsLoadingExportingToPdf(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPdf = async () => {
|
||||||
|
const productionResultPdfBlob = await pdf(
|
||||||
|
<ProductionResultReportPDF
|
||||||
|
mappedProductionResults={mappedProductionResults}
|
||||||
|
/>
|
||||||
|
).toBlob();
|
||||||
|
|
||||||
|
const productionResultReportPdfUrl = URL.createObjectURL(
|
||||||
|
productionResultPdfBlob
|
||||||
|
);
|
||||||
|
window.open(productionResultReportPdfUrl, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
await openPdf();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('Gagal melakukan export laporan hasil produksi! Coba lagi.');
|
||||||
|
}
|
||||||
|
// await ProductionResultReportApi.exportProductionResultToPdf(
|
||||||
|
// projectFlockKandangs
|
||||||
|
// );
|
||||||
|
|
||||||
|
setIsLoadingExportingToPdf(false);
|
||||||
|
};
|
||||||
|
|
||||||
const searchHandler = async () => {
|
const searchHandler = async () => {
|
||||||
setProjectFlockKandangs(null);
|
setProjectFlockKandangs(null);
|
||||||
setIsLoadingSearch(true);
|
setIsLoadingSearch(true);
|
||||||
@@ -235,6 +330,7 @@ const ProductionResultContent = () => {
|
|||||||
value={selectedArea}
|
value={selectedArea}
|
||||||
onChange={areaChangeHandler}
|
onChange={areaChangeHandler}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
@@ -251,6 +347,7 @@ const ProductionResultContent = () => {
|
|||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!selectedArea}
|
isDisabled={!selectedArea}
|
||||||
className={{
|
className={{
|
||||||
@@ -270,6 +367,7 @@ const ProductionResultContent = () => {
|
|||||||
value={selectedProjectFlock}
|
value={selectedProjectFlock}
|
||||||
onChange={projectFlockChangeHandler}
|
onChange={projectFlockChangeHandler}
|
||||||
onInputChange={setProjectFlockInputValue}
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!selectedArea || !selectedLocation}
|
isDisabled={!selectedArea || !selectedLocation}
|
||||||
className={{
|
className={{
|
||||||
@@ -289,6 +387,7 @@ const ProductionResultContent = () => {
|
|||||||
value={selectedProjectFlockKandang}
|
value={selectedProjectFlockKandang}
|
||||||
onChange={projectFlockKandangChangeHandler}
|
onChange={projectFlockKandangChangeHandler}
|
||||||
onInputChange={setProjectFlockKandangInputValue}
|
onInputChange={setProjectFlockKandangInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!selectedProjectFlock}
|
isDisabled={!selectedProjectFlock}
|
||||||
className={{
|
className={{
|
||||||
@@ -347,6 +446,13 @@ const ProductionResultContent = () => {
|
|||||||
onClick={exportToExcelHandler}
|
onClick={exportToExcelHandler}
|
||||||
className='text-nowrap'
|
className='text-nowrap'
|
||||||
/>
|
/>
|
||||||
|
<MenuItem
|
||||||
|
title='Export to PDF'
|
||||||
|
icon='icon-park-outline:file-pdf-one'
|
||||||
|
isLoading={isLoadingExportingToPdf}
|
||||||
|
onClick={exportToPdfHandler}
|
||||||
|
className='text-nowrap'
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Document,
|
||||||
|
Page,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
Image,
|
||||||
|
} from '@react-pdf/renderer';
|
||||||
|
|
||||||
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
|
import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||||
|
import { ProductionResult } from '@/types/api/report/production-result';
|
||||||
|
|
||||||
|
type MappedProductionResultsItem = {
|
||||||
|
projectFlockKandang: BaseProjectFlockKandang;
|
||||||
|
productionResult: ProductionResult[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProductionResultReportPDFProps {
|
||||||
|
mappedProductionResults?: MappedProductionResultsItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
page: {
|
||||||
|
paddingTop: 24,
|
||||||
|
paddingBottom: 52,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
},
|
||||||
|
|
||||||
|
companyInfoHeader: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
companyLogo: {
|
||||||
|
width: 64,
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
companyInfoHeaderDate: {
|
||||||
|
paddingTop: 8,
|
||||||
|
fontSize: 10,
|
||||||
|
},
|
||||||
|
companyName: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
companyAddress: {
|
||||||
|
fontSize: 8,
|
||||||
|
maxWidth: 420,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
doubleDivider: {
|
||||||
|
width: '100%',
|
||||||
|
height: 6,
|
||||||
|
borderTopWidth: 2,
|
||||||
|
borderTopColor: '#000',
|
||||||
|
borderBottomWidth: 2,
|
||||||
|
borderBottomColor: '#000',
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
marginTop: 14,
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '150%',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontFamily: 'Times-Roman',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
|
||||||
|
footer: {
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
position: 'absolute',
|
||||||
|
fontSize: 8,
|
||||||
|
bottom: 22,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'grey',
|
||||||
|
},
|
||||||
|
|
||||||
|
section: {
|
||||||
|
marginTop: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#000',
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
sectionHeader: {
|
||||||
|
marginBottom: 6,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
sectionSubtitle: {
|
||||||
|
fontSize: 8,
|
||||||
|
color: '#444',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Simple grid table (label/value pairs)
|
||||||
|
grid: {
|
||||||
|
width: '100%',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#000',
|
||||||
|
},
|
||||||
|
gridRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#000',
|
||||||
|
},
|
||||||
|
gridRowLast: {
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
gridCellLabel: {
|
||||||
|
width: '40%',
|
||||||
|
paddingVertical: 3,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
fontSize: 8,
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderRightColor: '#000',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
gridCellValue: {
|
||||||
|
width: '60%',
|
||||||
|
paddingVertical: 3,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
fontSize: 8,
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Subsection headings
|
||||||
|
groupTitle: {
|
||||||
|
marginTop: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
|
||||||
|
emptyText: {
|
||||||
|
fontSize: 8,
|
||||||
|
color: '#666',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function safeNum(v: unknown): number {
|
||||||
|
const n = typeof v === 'number' ? v : Number(v);
|
||||||
|
return Number.isFinite(n) ? n : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueText(v: unknown) {
|
||||||
|
if (v === null || v === undefined) return '-';
|
||||||
|
if (typeof v === 'number') return formatNumber(v);
|
||||||
|
return String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render label/value table for one ProductionResult.
|
||||||
|
* Uses a compact grid to keep page readable.
|
||||||
|
*/
|
||||||
|
function ProductionResultGrid({ pr }: { pr: ProductionResult }) {
|
||||||
|
const rows: Array<[string, string]> = [
|
||||||
|
['WOA', valueText(pr.woa)],
|
||||||
|
|
||||||
|
// BW
|
||||||
|
['BW', valueText(pr.bw)],
|
||||||
|
['Std BW', valueText(pr.std_bw)],
|
||||||
|
['Uniformity', valueText(pr.uniformity)],
|
||||||
|
['Std Uniformity', valueText(pr.std_uniformity)],
|
||||||
|
|
||||||
|
// Dep
|
||||||
|
['Dep Kum', valueText(pr.dep_kum)],
|
||||||
|
['Dep Std', valueText(pr.dep_std)],
|
||||||
|
|
||||||
|
// Butiran
|
||||||
|
['Butiran Utuh', valueText(pr.butiran_utuh)],
|
||||||
|
['Butiran Putih', valueText(pr.butiran_putih)],
|
||||||
|
['Butiran Retak', valueText(pr.butiran_retak)],
|
||||||
|
['Butiran Pecah', valueText(pr.butiran_pecah)],
|
||||||
|
['Butiran Jumlah', valueText(pr.butiran_jumlah)],
|
||||||
|
['Total Butir', valueText(pr.total_butir)],
|
||||||
|
|
||||||
|
// Kg
|
||||||
|
['Kg Utuh', valueText(pr.kg_utuh)],
|
||||||
|
['Kg Putih', valueText(pr.kg_putih)],
|
||||||
|
['Kg Retak', valueText(pr.kg_retak)],
|
||||||
|
['Kg Pecah', valueText(pr.kg_pecah)],
|
||||||
|
['Kg Jumlah', valueText(pr.kg_jumlah)],
|
||||||
|
['Total Kg', valueText(pr.total_kg)],
|
||||||
|
|
||||||
|
// %
|
||||||
|
['% Utuh', valueText(pr.persen_utuh)],
|
||||||
|
['% Putih', valueText(pr.persen_putih)],
|
||||||
|
['% Retak', valueText(pr.persen_retak)],
|
||||||
|
['% Pecah', valueText(pr.persen_pecah)],
|
||||||
|
|
||||||
|
// Produksi
|
||||||
|
['HD', valueText(pr.hd)],
|
||||||
|
['HD Std', valueText(pr.hd_std)],
|
||||||
|
['FI', valueText(pr.fi)],
|
||||||
|
['FI Std', valueText(pr.fi_std)],
|
||||||
|
['EM', valueText(pr.em)],
|
||||||
|
['EM Std', valueText(pr.em_std)],
|
||||||
|
['EW', valueText(pr.ew)],
|
||||||
|
['EW Std', valueText(pr.ew_std)],
|
||||||
|
['FCR', valueText(pr.fcr)],
|
||||||
|
['FCR Std', valueText(pr.fcr_std)],
|
||||||
|
['HH', valueText(pr.hh)],
|
||||||
|
['HH Std', valueText(pr.hh_std)],
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.grid}>
|
||||||
|
{rows.map(([label, value], idx) => {
|
||||||
|
const isLast = idx === rows.length - 1;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={label}
|
||||||
|
style={[styles.gridRow, ...(isLast ? [styles.gridRowLast] : [])]}
|
||||||
|
>
|
||||||
|
<Text style={styles.gridCellLabel}>{label}</Text>
|
||||||
|
<Text style={styles.gridCellValue}>{value}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there are multiple ProductionResult entries for a kandang,
|
||||||
|
* we show them sequentially with a small header per result.
|
||||||
|
*
|
||||||
|
* You can later change this to render only the latest WOA, or group by week.
|
||||||
|
*/
|
||||||
|
function ProductionResultList({
|
||||||
|
productionResults,
|
||||||
|
}: {
|
||||||
|
productionResults: ProductionResult[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{productionResults.map((pr, idx) => {
|
||||||
|
const kandangName =
|
||||||
|
pr.project_flock?.kandang?.name ||
|
||||||
|
pr.project_flock?.kandang?.id?.toString() ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
// Optional: show a compact subheader
|
||||||
|
const headerLeft = `Data #${idx + 1}`;
|
||||||
|
const headerRight =
|
||||||
|
kandangName && pr.woa !== undefined
|
||||||
|
? `${kandangName} • WOA ${safeNum(pr.woa)}`
|
||||||
|
: pr.woa !== undefined
|
||||||
|
? `WOA ${safeNum(pr.woa)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={`${pr.project_flock?.id ?? 'pf'}-${idx}`}
|
||||||
|
style={{ marginTop: idx === 0 ? 0 : 10 }}
|
||||||
|
wrap={false}
|
||||||
|
>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text style={styles.sectionTitle}>{headerLeft}</Text>
|
||||||
|
<Text style={styles.sectionSubtitle}>{headerRight}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ProductionResultGrid pr={pr} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Main PDF Component
|
||||||
|
*/
|
||||||
|
const ProductionResultReportPDF = ({
|
||||||
|
mappedProductionResults = [],
|
||||||
|
}: ProductionResultReportPDFProps) => {
|
||||||
|
return (
|
||||||
|
<Document>
|
||||||
|
<Page style={styles.page} size='A4'>
|
||||||
|
{/* Header */}
|
||||||
|
<View>
|
||||||
|
<View style={styles.companyInfoHeader}>
|
||||||
|
<Image style={styles.companyLogo} src='/assets/img/lti-logo.png' />
|
||||||
|
<Text style={styles.companyInfoHeaderDate}>
|
||||||
|
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Text style={styles.companyName}>PT LUMBUNG TELUR INDONESIA</Text>
|
||||||
|
<Text style={styles.companyAddress}>
|
||||||
|
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||||
|
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={styles.doubleDivider} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.title}>Laporan Production Result</Text>
|
||||||
|
|
||||||
|
{/* Sections per ProjectFlockKandang */}
|
||||||
|
{mappedProductionResults.length === 0 ? (
|
||||||
|
<View style={{ marginTop: 16 }}>
|
||||||
|
<Text style={styles.emptyText}>Tidak ada data.</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
mappedProductionResults.map((item, idx) => {
|
||||||
|
const pfk = item.projectFlockKandang;
|
||||||
|
|
||||||
|
// Try to display meaningful identifiers.
|
||||||
|
// Adjust these fields based on your real BaseProjectFlockKandang structure.
|
||||||
|
const kandangName =
|
||||||
|
pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`;
|
||||||
|
|
||||||
|
const projectName = pfk?.project_flock?.name ?? '';
|
||||||
|
|
||||||
|
const locationName = pfk?.project_flock?.location?.name ?? '';
|
||||||
|
|
||||||
|
const areaName = pfk?.project_flock?.area?.name ?? '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={`pfk-${pfk?.id ?? idx}`}
|
||||||
|
style={styles.section}
|
||||||
|
break={idx > 0} // each kandang starts on a new page for clarity
|
||||||
|
>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text style={styles.sectionTitle}>
|
||||||
|
{projectName
|
||||||
|
? `${projectName} • ${kandangName}`
|
||||||
|
: kandangName}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.sectionSubtitle}>
|
||||||
|
{[areaName, locationName].filter(Boolean).join(' • ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{item.productionResult && item.productionResult.length > 0 ? (
|
||||||
|
<ProductionResultList
|
||||||
|
productionResults={item.productionResult}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.emptyText}>
|
||||||
|
Tidak ada production result untuk kandang ini.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<View style={styles.footer} fixed>
|
||||||
|
<Text
|
||||||
|
render={({ pageNumber, totalPages }) =>
|
||||||
|
`${pageNumber} / ${totalPages}`
|
||||||
|
}
|
||||||
|
fixed
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductionResultReportPDF;
|
||||||
@@ -226,7 +226,7 @@ const createPDFDocument = (
|
|||||||
<Text>Rentang BW</Text>
|
<Text>Rentang BW</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
<Text>Sisa Ekor</Text>
|
<Text>Sisa Butir</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
<Text>Sisa Kg</Text>
|
<Text>Sisa Kg</Text>
|
||||||
@@ -234,12 +234,6 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>Rata-Rata Bobot (Kg)</Text>
|
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
|
||||||
<Text>Produksi Telur (Butir)</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
|
||||||
<Text>Produksi Telur (Kg)</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
<Text>Feed (Supplier)</Text>
|
<Text>Feed (Supplier)</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -249,16 +243,15 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>Rata-Rata Harga DOC</Text>
|
<Text>Rata-Rata Harga DOC</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
|
||||||
<Text>Nilai Nominal Telur</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
|
||||||
<Text>HPP Ayam</Text>
|
|
||||||
</View>
|
|
||||||
<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>
|
||||||
@@ -278,23 +271,15 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
||||||
<Text>{group.label}</Text>
|
<Text>{group.label}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
|
||||||
<Text>{formatNumber(group.remaining_chicken_birds)}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
|
||||||
<Text>
|
|
||||||
{formatNumber(group.remaining_chicken_weight_kg)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
|
||||||
<Text>{formatNumber(group.avg_weight_kg)}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
<Text>{formatNumber(group.egg_production_pieces)}</Text>
|
<Text>{formatNumber(group.egg_production_pieces)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
<Text>{formatNumber(group.egg_production_kg)}</Text>
|
<Text>{formatNumber(group.egg_production_kg)}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatNumber(group.avg_weight_kg)}</Text>
|
||||||
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
<Text>
|
<Text>
|
||||||
{group.feed_suppliers
|
{group.feed_suppliers
|
||||||
@@ -318,17 +303,16 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>{formatCurrency(group.average_doc_price_rp)}</Text>
|
<Text>{formatCurrency(group.average_doc_price_rp)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
|
||||||
<Text>{formatCurrency(group.egg_value_rp)}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
|
||||||
<Text>{formatCurrency(group.hpp_rp)}</Text>
|
|
||||||
</View>
|
|
||||||
<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
|
||||||
<Text>{formatCurrency(group.remaining_value_rp)}</Text>
|
style={[
|
||||||
|
pdfStyles.tableCellRight,
|
||||||
|
{ flex: 1.2, borderRightWidth: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>{formatCurrency(group.egg_value_rp)}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -356,16 +340,10 @@ const createPDFDocument = (
|
|||||||
<Text>Rata-Rata Bobot (Kg)</Text>
|
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
<Text>Sisa Ekor</Text>
|
<Text>Sisa Butir</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
<Text>Sisa Kg (Ayam)</Text>
|
<Text>Sisa Kg (Telur)</Text>
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
|
||||||
<Text>Produksi Telur (Butir)</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
|
||||||
<Text>Produksi Telur (Kg)</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
<Text>Feed (Supplier)</Text>
|
<Text>Feed (Supplier)</Text>
|
||||||
@@ -376,16 +354,15 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>Rata-Rata Harga DOC</Text>
|
<Text>Rata-Rata Harga DOC</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
|
||||||
<Text>Nilai Nominal Telur</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
|
||||||
<Text>HPP Ayam</Text>
|
|
||||||
</View>
|
|
||||||
<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>
|
||||||
@@ -394,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>
|
||||||
@@ -416,12 +388,6 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
<Text>{formatNumber(item.avg_weight_kg)}</Text>
|
<Text>{formatNumber(item.avg_weight_kg)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
|
||||||
<Text>{formatNumber(item.remaining_chicken_birds)}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
|
||||||
<Text>{formatNumber(item.remaining_chicken_weight_kg)}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
<Text>{formatNumber(item.egg_production_pieces)}</Text>
|
<Text>{formatNumber(item.egg_production_pieces)}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -451,20 +417,202 @@ const createPDFDocument = (
|
|||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>{formatCurrency(item.average_doc_price_rp)}</Text>
|
<Text>{formatCurrency(item.average_doc_price_rp)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
|
||||||
<Text>{formatCurrency(item.egg_value_rp)}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
|
||||||
<Text>{formatCurrency(item.hpp_rp)}</Text>
|
|
||||||
</View>
|
|
||||||
<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
|
||||||
<Text>{formatCurrency(item.remaining_value_rp)}</Text>
|
style={[
|
||||||
|
pdfStyles.tableCellRight,
|
||||||
|
{ flex: 1.2, borderRightWidth: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user