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",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"formik": "^2.4.6",
|
||||
"html-to-image": "^1.11.13",
|
||||
"input-otp": "^1.4.2",
|
||||
"jspdf": "^3.0.4",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
@@ -7380,6 +7381,12 @@
|
||||
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
|
||||
"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": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"formik": "^2.4.6",
|
||||
"html-to-image": "^1.11.13",
|
||||
"input-otp": "^1.4.2",
|
||||
"jspdf": "^3.0.4",
|
||||
"jspdf-autotable": "^5.0.2",
|
||||
|
||||
@@ -24,8 +24,6 @@ const FinanceDetailPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(finance);
|
||||
|
||||
// if (!finance || isResponseError(finance)) {
|
||||
// router.replace('/404');
|
||||
// return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Button, { ButtonProps } from '@/components/Button';
|
||||
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { FormikValues } from 'formik';
|
||||
|
||||
@@ -13,11 +14,12 @@ const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
||||
<Button
|
||||
{...props}
|
||||
onClick={onClick}
|
||||
className={
|
||||
className={cn(
|
||||
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'
|
||||
: ''
|
||||
}
|
||||
: '',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:funnel'
|
||||
|
||||
@@ -18,7 +18,7 @@ const AlertErrorList = ({
|
||||
if (formErrorList.length === 0) return null;
|
||||
|
||||
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 items-center gap-2'>
|
||||
<Icon icon='material-symbols:error-outline' width={24} height={24} />
|
||||
|
||||
@@ -113,7 +113,15 @@ const DateInput = ({
|
||||
};
|
||||
|
||||
const handleSelectSingle = (selectedDate?: Date) => {
|
||||
if (!selectedDate) return;
|
||||
if (!selectedDate) {
|
||||
setSelected(undefined);
|
||||
setDisplayValue('');
|
||||
const syntheticEvent = {
|
||||
target: { name, value: '' },
|
||||
} as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||
onChange?.(syntheticEvent);
|
||||
return;
|
||||
}
|
||||
if (minDate && selectedDate < minDate) {
|
||||
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
|
||||
return;
|
||||
@@ -136,7 +144,15 @@ const DateInput = ({
|
||||
};
|
||||
|
||||
const handleSelectRange = (range?: { from?: Date; to?: Date }) => {
|
||||
if (!range) return;
|
||||
if (!range) {
|
||||
setSelectedRange({});
|
||||
setDisplayValue('');
|
||||
const syntheticEvent = {
|
||||
target: { name, value: { from: '', to: '' } },
|
||||
} as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||
onChange?.(syntheticEvent);
|
||||
return;
|
||||
}
|
||||
setSelectedRange(range);
|
||||
|
||||
const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : '';
|
||||
|
||||
@@ -325,7 +325,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
};
|
||||
|
||||
const useSelect = <T,>(
|
||||
basePath: string,
|
||||
basePath: string | null,
|
||||
valueKey: keyof T | string,
|
||||
labelKey: keyof T | string,
|
||||
searchKey: string = 'search',
|
||||
@@ -354,7 +354,7 @@ const useSelect = <T,>(
|
||||
[limitKey]: String(limit),
|
||||
}).toString();
|
||||
|
||||
return `${basePath}?${qs}`;
|
||||
return basePath ? `${basePath}?${qs}` : null;
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@@ -3,224 +3,82 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import {
|
||||
DataSummarySubTotal,
|
||||
HppPurchaseData,
|
||||
ProfitLossDataAmount,
|
||||
} from '@/types/api/closing';
|
||||
import { HppItem, ProfitLossItem } from '@/types/api/closing';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
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 = ({
|
||||
projectFlockId,
|
||||
}: {
|
||||
projectFlockId: number;
|
||||
}) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: finance, isLoading } = useSWR(
|
||||
`/closing/finance/${projectFlockId}`,
|
||||
() => ClosingApi.getFinance(projectFlockId)
|
||||
`/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
||||
() =>
|
||||
ClosingApi.getFinance(
|
||||
projectFlockId,
|
||||
kandangId ? Number(kandangId) : undefined
|
||||
)
|
||||
);
|
||||
|
||||
const staticHppRows: Array<{
|
||||
group_name: string;
|
||||
type: string;
|
||||
group_index: number;
|
||||
}> = [
|
||||
{
|
||||
group_name: 'HPP dan Pengeluaran',
|
||||
type: 'Pembelian PAKAN',
|
||||
group_index: 0,
|
||||
},
|
||||
{
|
||||
group_name: 'HPP dan Pengeluaran',
|
||||
type: 'Pembelian STARTER',
|
||||
group_index: 0,
|
||||
},
|
||||
{
|
||||
group_name: 'HPP dan Pengeluaran',
|
||||
type: 'Pembelian DOC',
|
||||
group_index: 0,
|
||||
},
|
||||
{
|
||||
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: HppItem[] = useMemo(() => {
|
||||
if (isResponseSuccess(finance)) {
|
||||
const customItems = {
|
||||
label: 'HPP dan Pengeluaran',
|
||||
code: 'custom_row',
|
||||
} as HppItem;
|
||||
const purchases = finance.data.hpp.items.filter(
|
||||
(item) => item.category === 'purchase'
|
||||
);
|
||||
const totalBudgeting = {
|
||||
label: 'HPP dan Bahan Baku',
|
||||
code: 'custom_row',
|
||||
} as HppItem;
|
||||
const overheads = finance.data.hpp.items.filter(
|
||||
(item) => item.category === 'overhead'
|
||||
);
|
||||
return [customItems, ...purchases, totalBudgeting, ...overheads];
|
||||
}
|
||||
return [];
|
||||
}, [finance]);
|
||||
|
||||
const hppTableData: HppTableRow[] = [
|
||||
{
|
||||
group_name: 'HPP dan Pengeluaran',
|
||||
group_index: 0,
|
||||
isGroupHeader: true as const,
|
||||
},
|
||||
...staticHppRows
|
||||
.filter((row) => row.group_index === 0)
|
||||
.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 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,
|
||||
const profitLossTableData: ProfitLossItem[] = useMemo(() => {
|
||||
if (isResponseSuccess(finance)) {
|
||||
const incomes = finance.data.profit_loss.items.filter(
|
||||
(item) => item.type === 'income'
|
||||
);
|
||||
const purchases = finance.data.profit_loss.items.filter(
|
||||
(item) => item.type === 'purchase'
|
||||
);
|
||||
const overheads = finance.data.profit_loss.items.filter(
|
||||
(item) => item.type === 'overhead'
|
||||
);
|
||||
const grossProfit = {
|
||||
label: 'LABA RUGI BRUTO',
|
||||
code: 'custom_row',
|
||||
type: 'gross_profit',
|
||||
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,
|
||||
finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
|
||||
rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
|
||||
amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
|
||||
} as ProfitLossItem;
|
||||
const subtotal = {
|
||||
label: 'Subtotal',
|
||||
code: 'custom_row',
|
||||
type: 'subtotal',
|
||||
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,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
|
||||
rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
|
||||
amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
|
||||
} as ProfitLossItem;
|
||||
return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
|
||||
}
|
||||
return [];
|
||||
}, [finance]);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-4'>
|
||||
@@ -233,35 +91,21 @@ const ClosingFinanceTable = ({
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-6'>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div>
|
||||
{isResponseSuccess(finance)
|
||||
? formatTitleCase(
|
||||
finance.data.profit_loss.data.summary.gross_profit
|
||||
.label || '-'
|
||||
)
|
||||
: 'Laba Rugi Brutto'}
|
||||
</div>
|
||||
<div>Laba Rugi Brutto</div>
|
||||
<div className='text-lg font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.gross_profit.amount
|
||||
finance.data.profit_loss.summary.gross_profit.amount
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div>
|
||||
{isResponseSuccess(finance)
|
||||
? formatTitleCase(
|
||||
finance.data.profit_loss.data.summary.net_profit.label ||
|
||||
'-'
|
||||
)
|
||||
: 'Laba Rugi Netto'}
|
||||
</div>
|
||||
<div>Laba Rugi Netto</div>
|
||||
<div className='text-lg font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.net_profit.amount
|
||||
finance.data.profit_loss.summary.net_profit.amount
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
@@ -269,11 +113,7 @@ const ClosingFinanceTable = ({
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
isResponseSuccess(finance)
|
||||
? finance.data.hpp_purchases.title
|
||||
: 'HPP Purchases'
|
||||
}
|
||||
title='HPP Purchases'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
className={{
|
||||
@@ -281,17 +121,18 @@ const ClosingFinanceTable = ({
|
||||
}}
|
||||
>
|
||||
<div className='mt-6 p-0 mb-0'>
|
||||
<Table<HppTableRow>
|
||||
<Table<HppItem>
|
||||
data={hppTableData}
|
||||
isLoading={isLoading}
|
||||
columns={[
|
||||
{
|
||||
header: 'No.',
|
||||
enableSorting: false,
|
||||
accessorFn: (item, index) => {
|
||||
if (item.isGroupHeader) return '-';
|
||||
if (item.code === 'custom_row') return '-';
|
||||
const dataRowsBefore = hppTableData
|
||||
.slice(0, index)
|
||||
.filter((row) => !row.isGroupHeader).length;
|
||||
.filter((row) => row.code !== 'custom_row').length;
|
||||
return dataRowsBefore + 1;
|
||||
},
|
||||
footer: (props) => {
|
||||
@@ -301,7 +142,7 @@ const ClosingFinanceTable = ({
|
||||
{
|
||||
header: 'Jenis',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatTitleCase(item.type || '-'),
|
||||
accessorFn: (item) => formatTitleCase(item.label || '-'),
|
||||
},
|
||||
{
|
||||
header: 'Budgeting',
|
||||
@@ -317,7 +158,7 @@ const ClosingFinanceTable = ({
|
||||
return props.column.id === 'budgeting_rp_per_bird' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp?.budgeting
|
||||
finance.data.hpp.summary?.budgeting
|
||||
?.rp_per_bird || 0
|
||||
)
|
||||
: '-';
|
||||
@@ -333,8 +174,8 @@ const ClosingFinanceTable = ({
|
||||
return props.column.id === 'budgeting_rp_per_kg' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp?.budgeting
|
||||
?.rp_per_kg || 0
|
||||
finance.data.hpp.summary?.budgeting?.rp_per_kg ||
|
||||
0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
@@ -349,8 +190,7 @@ const ClosingFinanceTable = ({
|
||||
return props.column.id === 'budgeting_amount' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp?.budgeting
|
||||
?.amount || 0
|
||||
finance.data.hpp.summary?.budgeting?.amount || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
@@ -371,8 +211,8 @@ const ClosingFinanceTable = ({
|
||||
return props.column.id === 'realization_rp_per_bird' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp
|
||||
?.realization?.rp_per_bird || 0
|
||||
finance.data.hpp.summary?.realization
|
||||
?.rp_per_bird || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
@@ -387,8 +227,8 @@ const ClosingFinanceTable = ({
|
||||
return props.column.id === 'realization_rp_per_kg' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp
|
||||
?.realization?.rp_per_kg || 0
|
||||
finance.data.hpp.summary?.realization
|
||||
?.rp_per_kg || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
@@ -403,8 +243,7 @@ const ClosingFinanceTable = ({
|
||||
return props.column.id === 'realization_amount' &&
|
||||
isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.hpp_purchases.summary_hpp
|
||||
?.realization?.amount || 0
|
||||
finance.data.hpp.summary?.realization?.amount || 0
|
||||
)
|
||||
: '-';
|
||||
},
|
||||
@@ -414,7 +253,7 @@ const ClosingFinanceTable = ({
|
||||
]}
|
||||
renderCustomRow={(row) => {
|
||||
const rowData = row.original;
|
||||
if (rowData.isGroupHeader) {
|
||||
if (rowData.code === 'custom_row') {
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
@@ -428,7 +267,7 @@ const ClosingFinanceTable = ({
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatTitleCase(rowData.group_name ?? '-')}
|
||||
{formatTitleCase(rowData.label ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -441,11 +280,7 @@ const ClosingFinanceTable = ({
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
isResponseSuccess(finance)
|
||||
? finance.data.profit_loss.title
|
||||
: 'Profit/Loss'
|
||||
}
|
||||
title='Profit/Loss'
|
||||
variant='bordered'
|
||||
collapsible
|
||||
className={{
|
||||
@@ -453,38 +288,32 @@ const ClosingFinanceTable = ({
|
||||
}}
|
||||
>
|
||||
<div className='mt-6 p-0 mb-0'>
|
||||
<Table<ProfitLossTableRow>
|
||||
<Table<ProfitLossItem>
|
||||
data={profitLossTableData}
|
||||
isLoading={isLoading}
|
||||
columns={[
|
||||
{
|
||||
header: 'Jenis',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => item.type,
|
||||
accessorFn: (item) => item.label,
|
||||
cell: (item) => (
|
||||
<div className=''>
|
||||
{formatTitleCase(item.row.original.type || '-')}
|
||||
{formatTitleCase(item.row.original.label || '-')}
|
||||
</div>
|
||||
),
|
||||
footer: (item) => (
|
||||
<div className='font-bold uppercase'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatTitleCase(
|
||||
finance.data.profit_loss.data.summary.net_profit
|
||||
.label || '-'
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
footer: () => (
|
||||
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Rp/Ekor',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
||||
footer: (item) => (
|
||||
footer: () => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.net_profit
|
||||
finance.data.profit_loss.summary.net_profit
|
||||
.rp_per_bird || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
@@ -495,11 +324,11 @@ const ClosingFinanceTable = ({
|
||||
header: 'Rp/Kg',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
||||
footer: (item) => (
|
||||
footer: () => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.net_profit
|
||||
finance.data.profit_loss.summary.net_profit
|
||||
.rp_per_kg || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
@@ -510,11 +339,11 @@ const ClosingFinanceTable = ({
|
||||
header: 'Jumlah (Rp)',
|
||||
enableSorting: false,
|
||||
accessorFn: (item) => formatCurrency(item.amount || 0),
|
||||
footer: (item) => (
|
||||
footer: () => (
|
||||
<div className='font-bold'>
|
||||
{isResponseSuccess(finance)
|
||||
? formatCurrency(
|
||||
finance.data.profit_loss.data.summary.net_profit
|
||||
finance.data.profit_loss.summary.net_profit
|
||||
.amount || 0
|
||||
)
|
||||
: formatCurrency(0)}
|
||||
@@ -524,37 +353,28 @@ const ClosingFinanceTable = ({
|
||||
]}
|
||||
renderCustomRow={(row) => {
|
||||
const rowData = row.original;
|
||||
if (rowData.isGroupHeader) {
|
||||
if (rowData.amount) {
|
||||
if (rowData.code === 'custom_row') {
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
||||
>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||
<div className='font-bold ps-6 uppercase'>
|
||||
{formatTitleCase(rowData.label ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||
<div className='font-bold'>
|
||||
{formatCurrency(rowData.amount ?? 0)}
|
||||
</div>
|
||||
@@ -562,22 +382,6 @@ const ClosingFinanceTable = ({
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<tr
|
||||
key={row.id}
|
||||
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
||||
>
|
||||
<td
|
||||
colSpan={4}
|
||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||
>
|
||||
<div className='font-bold'>
|
||||
{formatTitleCase(rowData.group_name ?? '-')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
className={{
|
||||
|
||||
@@ -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';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
|
||||
@@ -23,6 +24,9 @@ interface ClosingIncomingSapronaksTableProps {
|
||||
const ClosingIncomingSapronaksTable = ({
|
||||
projectFlockId,
|
||||
}: ClosingIncomingSapronaksTableProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -43,7 +47,7 @@ const ClosingIncomingSapronaksTable = ({
|
||||
|
||||
const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } =
|
||||
useSWR(
|
||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming`,
|
||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||
ClosingApi.getAllIncomingSapronakFetcher,
|
||||
{
|
||||
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';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
|
||||
@@ -23,6 +24,9 @@ interface ClosingOutgoingSapronaksTableProps {
|
||||
const ClosingOutgoingSapronaksTable = ({
|
||||
projectFlockId,
|
||||
}: ClosingOutgoingSapronaksTableProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -43,7 +47,7 @@ const ClosingOutgoingSapronaksTable = ({
|
||||
|
||||
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
||||
useSWR(
|
||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing`,
|
||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||
ClosingApi.getAllOutgoingSapronakFetcher,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
|
||||
@@ -32,17 +32,11 @@ const ClosingOverheadTable = ({
|
||||
);
|
||||
|
||||
// Helper function to create columns with footer support
|
||||
const createColumns = (total?: OverheadTotal): ColumnDef<Overhead>[] => [
|
||||
// Group untuk kolom tanpa footer
|
||||
{
|
||||
header: 'Nama Item',
|
||||
accessorFn: (props) => props.item_name,
|
||||
footer: 'Total Pengeluaran Overhead',
|
||||
},
|
||||
{
|
||||
header: 'Satuan',
|
||||
accessorFn: (props) => props.uom_name,
|
||||
},
|
||||
const createColumns = (
|
||||
total?: OverheadTotal,
|
||||
kandangId?: number
|
||||
): ColumnDef<Overhead>[] => {
|
||||
const flockColumn: ColumnDef<Overhead>[] = [
|
||||
{
|
||||
header: 'Budget Pengajuan',
|
||||
footer: '',
|
||||
@@ -70,7 +64,9 @@ const ClosingOverheadTable = ({
|
||||
props.budget_total_amount
|
||||
? formatCurrency(props.budget_total_amount)
|
||||
: '-',
|
||||
footer: total ? () => formatCurrency(total.budget_total_amount) : '',
|
||||
footer: total
|
||||
? () => formatCurrency(total.budget_total_amount)
|
||||
: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -110,10 +106,68 @@ const ClosingOverheadTable = ({
|
||||
props.actual_total_amount
|
||||
? formatCurrency(props.actual_total_amount)
|
||||
: '-',
|
||||
footer: total ? () => formatCurrency(total.actual_total_amount) : '',
|
||||
footer: total
|
||||
? () => formatCurrency(total.actual_total_amount)
|
||||
: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const kandangColumn: ColumnDef<Overhead>[] = [
|
||||
{
|
||||
id: 'actual_date',
|
||||
header: 'Tanggal',
|
||||
accessorFn: (props) =>
|
||||
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',
|
||||
@@ -122,11 +176,16 @@ const ClosingOverheadTable = ({
|
||||
footer: total ? () => formatCurrency(total.cost_per_bird) : '',
|
||||
},
|
||||
];
|
||||
return finalColumns;
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(overhead)
|
||||
? createColumns(overhead.data?.total)
|
||||
? createColumns(
|
||||
overhead.data?.total,
|
||||
kandangId ? Number(kandangId) : undefined
|
||||
)
|
||||
: createColumns(),
|
||||
[overhead]
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { ClosingApi } from '@/services/api/closing';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
@@ -12,9 +13,12 @@ interface ClosingProductionDataTabContentProps {
|
||||
const ClosingProductionDataTabContent = ({
|
||||
projectFlockId,
|
||||
}: ClosingProductionDataTabContentProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const kandangId = searchParams.get('kandangId');
|
||||
|
||||
const { data: productionData, isLoading } = useSWR(
|
||||
`${ClosingApi.basePath}/${projectFlockId}/production-data`,
|
||||
() => ClosingApi.getProductionData(projectFlockId)
|
||||
`${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||
() => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
|
||||
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
||||
import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable';
|
||||
import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable';
|
||||
|
||||
interface ClosingSapronakTableProps {
|
||||
projectFlockId?: number;
|
||||
@@ -16,7 +18,15 @@ const ClosingSapronakTabContent = ({
|
||||
<>
|
||||
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
|
||||
|
||||
<ClosingIncomingSapronaksSummaryTable
|
||||
projectFlockId={projectFlockId}
|
||||
/>
|
||||
|
||||
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
|
||||
|
||||
<ClosingOutgoingSapronaksSummaryTable
|
||||
projectFlockId={projectFlockId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -163,6 +163,7 @@ const ClosingsTable = () => {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
@@ -228,6 +229,7 @@ const ClosingsTable = () => {
|
||||
value={selectedLocation}
|
||||
onChange={locationChangeHandler}
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6',
|
||||
|
||||
@@ -82,12 +82,12 @@ const SalesReportTable = ({
|
||||
<div className='font-semibold text-gray-900'>Total Penjualan</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'age',
|
||||
accessorKey: 'age',
|
||||
header: 'Umur',
|
||||
cell: (props) => props.getValue() || '-',
|
||||
},
|
||||
// {
|
||||
// id: 'age',
|
||||
// accessorKey: 'age',
|
||||
// header: 'Umur',
|
||||
// cell: (props) => props.getValue() || '-',
|
||||
// },
|
||||
{
|
||||
id: 'do_number',
|
||||
accessorKey: 'do_number',
|
||||
|
||||
@@ -8,19 +8,22 @@ import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { DashboardApi } from '@/services/api/dashboard';
|
||||
import { useFormik } from 'formik';
|
||||
import { ProjectFlockApi } from '@/services/api/production';
|
||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||
|
||||
import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF';
|
||||
import {
|
||||
DashboardFilterType,
|
||||
getDashboardFilterSchema,
|
||||
} from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
||||
import DashboardLineChart from '@/components/pages/dashboard/chart/DashboardLineChart';
|
||||
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 {
|
||||
DashboardFilter,
|
||||
@@ -30,6 +33,11 @@ import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
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
|
||||
const normalizeToArray = (
|
||||
@@ -44,11 +52,22 @@ const normalizeToArray = (
|
||||
|
||||
const DashboardProduction = () => {
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== DASHBOARD STORE =====
|
||||
const { filterValues, setFilterValues, resetFilterValues } =
|
||||
useDashboardStore();
|
||||
|
||||
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
||||
'OVERVIEW'
|
||||
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
|
||||
);
|
||||
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 =====
|
||||
const {
|
||||
@@ -64,19 +83,29 @@ const DashboardProduction = () => {
|
||||
: undefined;
|
||||
|
||||
// ===== SELECT =====
|
||||
const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } =
|
||||
useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
|
||||
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,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocation,
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
const { options: kandangOptions, isLoadingOptions: isLoadingKandangOptions } =
|
||||
useSelect(KandangApi.basePath, 'id', 'name', '', {
|
||||
const {
|
||||
setInputValue: setInputValueKandang,
|
||||
options: kandangOptions,
|
||||
isLoadingOptions: isLoadingKandangOptions,
|
||||
loadMore: loadMoreKandang,
|
||||
} = useSelect(KandangApi.basePath, 'id', 'name', '', {
|
||||
limit: 'limit',
|
||||
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
||||
});
|
||||
@@ -89,20 +118,21 @@ const DashboardProduction = () => {
|
||||
// ===== FORMIK =====
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
flock: [] as OptionType[],
|
||||
location: [] as OptionType[],
|
||||
kandang: [] as OptionType[],
|
||||
analysisMode: analysisMode,
|
||||
comparisonType: '',
|
||||
lokasiIds: [],
|
||||
flockIds: [],
|
||||
kandangIds: [],
|
||||
startDate: filterValues.startDate || '',
|
||||
endDate: filterValues.endDate || '',
|
||||
flock: filterValues.flock || ([] as OptionType[]),
|
||||
location: filterValues.location || ([] as OptionType[]),
|
||||
kandang: filterValues.kandang || ([] as OptionType[]),
|
||||
analysisMode: filterValues.analysisMode || analysisMode,
|
||||
comparisonType: filterValues.comparisonType || '',
|
||||
locationIds: filterValues.locationIds || [],
|
||||
flockIds: filterValues.flockIds || [],
|
||||
kandangIds: filterValues.kandangIds || [],
|
||||
} as DashboardFilterType,
|
||||
validationSchema: getDashboardFilterSchema(analysisMode),
|
||||
onSubmit: (values) => {
|
||||
console.log(values);
|
||||
// Save filter values to store
|
||||
setFilterValues(values);
|
||||
|
||||
handleApplyFilter({
|
||||
start_date: values.startDate || '',
|
||||
@@ -118,13 +148,13 @@ const DashboardProduction = () => {
|
||||
|
||||
const handleResetFilter = () => {
|
||||
formik.resetForm();
|
||||
resetFilterValues(); // Clear stored filter values
|
||||
setAnalysisMode('OVERVIEW');
|
||||
setEndpointUrl('/dashboards');
|
||||
setSelectedLocationIds([]);
|
||||
};
|
||||
|
||||
const handleApplyFilter = (values: DashboardFilter) => {
|
||||
console.log(values);
|
||||
|
||||
// Build query params object, only include non-empty values
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
@@ -140,15 +170,37 @@ const DashboardProduction = () => {
|
||||
if (values.comparison_type) params.comparison_type = values.comparison_type;
|
||||
|
||||
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
|
||||
console.log(endpointUrl);
|
||||
filterModal.closeModal();
|
||||
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 =====
|
||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||
|
||||
// ===== Export PDF =====
|
||||
const handleExportPDF = async () => {
|
||||
await generateDashboardPDF({
|
||||
filterValues: formik.values,
|
||||
statsRef,
|
||||
allChartsRef,
|
||||
setExporting,
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoadingDashboardProductionData) {
|
||||
return (
|
||||
<div className='w-full min-h-screen flex items-center justify-center'>
|
||||
@@ -156,78 +208,51 @@ const DashboardProduction = () => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full p-4 space-y-6'>
|
||||
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
|
||||
<div></div>
|
||||
|
||||
<div className='flex flex-row justify-end gap-2'>
|
||||
<Button
|
||||
<ButtonFilter
|
||||
values={{
|
||||
...formik.values,
|
||||
analysisMode: undefined,
|
||||
}}
|
||||
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'
|
||||
>
|
||||
<Icon icon='heroicons:arrow-down-tray' width={20} height={20} />
|
||||
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' width={20} height={20} />
|
||||
<Icon icon='heroicons:chevron-down' />
|
||||
</Button>
|
||||
}
|
||||
className={{
|
||||
content: 'w-full',
|
||||
}}
|
||||
>
|
||||
<Menu className={exporting ? 'hidden' : ''}>
|
||||
<MenuItem title='PDF' onClick={handleExportPDF} />
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dashboard Stats */}
|
||||
<DashboardStats data={dashboardProductionData?.statistics_data ?? []} />
|
||||
<div ref={statsRef}>
|
||||
<DashboardStats
|
||||
data={dashboardProductionData?.statistics_data ?? []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Use DashboardLineChart component or skeleton */}
|
||||
<div ref={chartRef}>
|
||||
{isLoadingDashboardProductionData ? (
|
||||
<DashboardLineChartSkeleton />
|
||||
) : dashboardProductionData &&
|
||||
@@ -244,6 +269,11 @@ const DashboardProduction = () => {
|
||||
: analysisMode
|
||||
}
|
||||
data={dashboardProductionData}
|
||||
selectedKandang={
|
||||
analysisMode === 'OVERVIEW'
|
||||
? (formik.values.kandang as OptionType)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<DashboardLineChartSkeleton
|
||||
@@ -254,6 +284,33 @@ const DashboardProduction = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<Modal
|
||||
@@ -287,7 +344,7 @@ const DashboardProduction = () => {
|
||||
{/* Rentang Waktu */}
|
||||
<div className='px-4'>
|
||||
<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
|
||||
name='startDate'
|
||||
placeholder='Tanggal Mulai'
|
||||
@@ -302,7 +359,7 @@ const DashboardProduction = () => {
|
||||
Boolean(formik.touched.startDate)
|
||||
}
|
||||
/>
|
||||
<span className='hidden md:block text-center'>—</span>
|
||||
<div className='hidden md:block mt-3 text-center'>—</div>
|
||||
<DateInput
|
||||
name='endDate'
|
||||
placeholder='Tanggal Akhir'
|
||||
@@ -383,6 +440,8 @@ const DashboardProduction = () => {
|
||||
<SelectInput
|
||||
label='Farm'
|
||||
value={formik.values.location}
|
||||
onInputChange={setInputValueLocation}
|
||||
onMenuScrollToBottom={loadMoreLocation}
|
||||
onChange={(selected) => {
|
||||
formik.setFieldValue('location', selected);
|
||||
// Update selectedLocationIds for kandang filter
|
||||
@@ -422,6 +481,8 @@ const DashboardProduction = () => {
|
||||
formik.setFieldValue('flock', selected)
|
||||
}
|
||||
errorMessage={formik.errors.flock as string}
|
||||
onInputChange={setInputValueFlock}
|
||||
onMenuScrollToBottom={loadMoreFlock}
|
||||
options={flockOptions}
|
||||
isLoading={isLoadingFlockOptions}
|
||||
isMulti={
|
||||
@@ -450,6 +511,8 @@ const DashboardProduction = () => {
|
||||
formik.setFieldValue('kandang', selected)
|
||||
}
|
||||
errorMessage={formik.errors.kandang as string}
|
||||
onInputChange={setInputValueKandang}
|
||||
onMenuScrollToBottom={loadMoreKandang}
|
||||
options={kandangOptions}
|
||||
isLoading={isLoadingKandangOptions}
|
||||
isMulti={
|
||||
@@ -465,7 +528,9 @@ const DashboardProduction = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='w-full p-4'>
|
||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<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'
|
||||
variant='soft'
|
||||
className='ms-4 min-w-36 rounded-lg'
|
||||
onClick={handleResetFilter}
|
||||
>
|
||||
Reset Filter
|
||||
</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 Card from '@/components/Card';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
import {
|
||||
Dashboard,
|
||||
DashboardOverviewCharts,
|
||||
@@ -25,20 +27,29 @@ import {
|
||||
type DashboardLineChartProps = {
|
||||
analysisMode: 'OVERVIEW' | 'COMPARISON';
|
||||
data: Dashboard;
|
||||
selectedKandang?: OptionType;
|
||||
};
|
||||
|
||||
// Type guard to check if charts is DashboardOverviewCharts
|
||||
function isOverviewCharts(
|
||||
charts: DashboardOverviewCharts | DashboardComparisonCharts
|
||||
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||
): 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
|
||||
function isComparisonCharts(
|
||||
charts: DashboardOverviewCharts | DashboardComparisonCharts
|
||||
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||
): 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> = {
|
||||
@@ -94,6 +105,7 @@ const getLineColor = (
|
||||
const DashboardLineChart = ({
|
||||
analysisMode,
|
||||
data,
|
||||
selectedKandang,
|
||||
}: DashboardLineChartProps) => {
|
||||
const [chartData, setChartData] =
|
||||
useState<keyof DashboardOverviewCharts>('body_weight');
|
||||
@@ -123,7 +135,7 @@ const DashboardLineChart = ({
|
||||
isComparisonCharts(data.charts)
|
||||
) {
|
||||
const comparisonChart =
|
||||
data.charts.location || data.charts.flock || data.charts.kandang;
|
||||
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||
seriesData = comparisonChart?.series || [];
|
||||
}
|
||||
|
||||
@@ -224,7 +236,7 @@ const DashboardLineChart = ({
|
||||
isComparisonCharts(data.charts)
|
||||
) {
|
||||
const comparisonChart =
|
||||
data.charts.location || data.charts.flock || data.charts.kandang;
|
||||
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||
seriesData = comparisonChart?.series || [];
|
||||
}
|
||||
|
||||
@@ -283,6 +295,8 @@ const DashboardLineChart = ({
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Chart Container with Empty State Overlay */}
|
||||
<div className='relative'>
|
||||
{/* Chart */}
|
||||
<ResponsiveContainer width='100%' height={350}>
|
||||
<LineChart
|
||||
@@ -292,7 +306,8 @@ const DashboardLineChart = ({
|
||||
// For OVERVIEW mode, use the selected chart data
|
||||
if (isOverviewCharts(data.charts)) {
|
||||
const selectedChartData = data.charts[chartData];
|
||||
if (!selectedChartData || !selectedChartData.dataset) return [];
|
||||
if (!selectedChartData || !selectedChartData.dataset)
|
||||
return [];
|
||||
return selectedChartData.dataset;
|
||||
}
|
||||
return [];
|
||||
@@ -300,7 +315,7 @@ const DashboardLineChart = ({
|
||||
// For COMPARISON mode, use the first available comparison chart
|
||||
if (isComparisonCharts(data.charts)) {
|
||||
const chartData =
|
||||
data.charts.location ||
|
||||
data.charts.farm ||
|
||||
data.charts.flock ||
|
||||
data.charts.kandang;
|
||||
|
||||
@@ -350,7 +365,7 @@ const DashboardLineChart = ({
|
||||
isComparisonCharts(data.charts)
|
||||
) {
|
||||
const comparisonChart =
|
||||
data.charts.location ||
|
||||
data.charts.farm ||
|
||||
data.charts.flock ||
|
||||
data.charts.kandang;
|
||||
seriesData = comparisonChart?.series || [];
|
||||
@@ -398,7 +413,7 @@ const DashboardLineChart = ({
|
||||
isComparisonCharts(data.charts)
|
||||
) {
|
||||
const comparisonChart =
|
||||
data.charts.location ||
|
||||
data.charts.farm ||
|
||||
data.charts.flock ||
|
||||
data.charts.kandang;
|
||||
seriesData = comparisonChart?.series || [];
|
||||
@@ -449,11 +464,84 @@ const DashboardLineChart = ({
|
||||
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={(
|
||||
value: number | undefined,
|
||||
name: string | undefined
|
||||
) => {
|
||||
if (value === undefined || name === undefined) return ['', ''];
|
||||
if (
|
||||
value === undefined ||
|
||||
name === undefined ||
|
||||
name.startsWith('STD. ')
|
||||
)
|
||||
return [undefined, undefined];
|
||||
|
||||
// Get series data to find the unit
|
||||
let seriesData: DashboardChartsSeries[] = [];
|
||||
@@ -467,7 +555,7 @@ const DashboardLineChart = ({
|
||||
isComparisonCharts(data.charts)
|
||||
) {
|
||||
const comparisonChart =
|
||||
data.charts.location ||
|
||||
data.charts.farm ||
|
||||
data.charts.flock ||
|
||||
data.charts.kandang;
|
||||
seriesData = comparisonChart?.series || [];
|
||||
@@ -475,25 +563,26 @@ const DashboardLineChart = ({
|
||||
|
||||
// Find the series that matches this line's name
|
||||
const series = seriesData.find((s) => s.label === name);
|
||||
const unit = series?.unit || '';
|
||||
const id = series?.id || '';
|
||||
|
||||
return [`${value} ${unit}`, name];
|
||||
return [value, id];
|
||||
}}
|
||||
/>
|
||||
{/* Dynamic Line rendering based on visible series */}
|
||||
{(() => {
|
||||
let seriesData: DashboardChartsSeries[] = [];
|
||||
|
||||
if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) {
|
||||
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;
|
||||
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||
seriesData = comparisonChart?.series || [];
|
||||
}
|
||||
|
||||
@@ -538,6 +627,50 @@ const DashboardLineChart = ({
|
||||
})()}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
comparisonType: string | undefined;
|
||||
location: OptionType | OptionType[];
|
||||
lokasiIds: number[] | undefined;
|
||||
locationIds: number[] | undefined;
|
||||
flock: OptionType | OptionType[] | undefined;
|
||||
flockIds: number[] | undefined;
|
||||
kandang: OptionType | OptionType[] | undefined;
|
||||
@@ -25,7 +25,7 @@ export const DashboardFilterOverviewSchema: yup.ObjectSchema<DashboardFilterType
|
||||
then: (schema) => schema.required('Compared by is required'),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
lokasiIds: yup.array().optional(),
|
||||
locationIds: yup.array().optional(),
|
||||
flockIds: yup.array().optional(),
|
||||
kandangIds: yup.array().optional(),
|
||||
location: yup
|
||||
@@ -68,7 +68,7 @@ export const DashboardFilterComparisonSchema: yup.ObjectSchema<DashboardFilterTy
|
||||
then: (schema) => schema.required('Compared by is required'),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
lokasiIds: yup.array().optional(),
|
||||
locationIds: yup.array().optional(),
|
||||
flockIds: yup.array().optional(),
|
||||
kandangIds: yup.array().optional(),
|
||||
location: yup
|
||||
|
||||
@@ -43,7 +43,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full max-w-7xl pb-16'>
|
||||
<section className='w-full max-w-full pb-16'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/expense'
|
||||
@@ -65,7 +65,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
||||
tabs={expenseDetailTabs}
|
||||
variant='lifted'
|
||||
className={{
|
||||
wrapper: 'max-w-5xl mx-auto mt-4',
|
||||
wrapper: 'mx-auto mt-4',
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -68,7 +68,7 @@ const ExpenseRealizationContent = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||
<RequirePermission permissions='lti.expense.update.realization'>
|
||||
<Button
|
||||
@@ -84,7 +84,7 @@ const ExpenseRealizationContent = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='overflow-x-auto w-full max-w-5xl mx-auto'>
|
||||
<div className='overflow-x-auto w-full mx-auto'>
|
||||
<table className='table table-sm table-zebra'>
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -179,7 +179,7 @@ const ExpenseRealizationContent = ({
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
||||
<div className='w-full mt-8 mx-auto'>
|
||||
<div className='flex flex-row gap-4'>
|
||||
<Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}>
|
||||
<div className='w-full flex flex-col gap-2'>
|
||||
@@ -216,13 +216,15 @@ const ExpenseRealizationContent = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
||||
<div className='w-full mt-8 mx-auto grid grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<h2 className='font-bold text-xl text-center'>
|
||||
Rincian Pengajuan Biaya Operasional
|
||||
</h2>
|
||||
|
||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
||||
{initialValues?.kandangs.map(
|
||||
(kandangExpense, kandangExpenseIdx) => {
|
||||
let expenseGrandTotal = 0;
|
||||
|
||||
kandangExpense.pengajuans?.forEach(
|
||||
@@ -258,7 +260,9 @@ const ExpenseRealizationContent = ({
|
||||
<td>{pengajuanItem.nonstock.name}</td>
|
||||
<td>{pengajuanItem.qty}</td>
|
||||
<td>{formatCurrency(pengajuanItem.price)}</td>
|
||||
<td className='w-xs'>{pengajuanItem.note ?? '-'}</td>
|
||||
<td className='w-xs'>
|
||||
{pengajuanItem.notes ?? '-'}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
@@ -268,23 +272,27 @@ const ExpenseRealizationContent = ({
|
||||
<th colSpan={2} className='text-right'>
|
||||
Total Biaya Keseluruhan:
|
||||
</th>
|
||||
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th>
|
||||
<th colSpan={2}>
|
||||
{formatCurrency(expenseGrandTotal)}
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
||||
<div>
|
||||
<h2 className='font-bold text-xl text-center'>
|
||||
Rincian Realisasi Biaya Operasional
|
||||
</h2>
|
||||
|
||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
||||
{initialValues?.kandangs.map(
|
||||
(kandangExpense, kandangExpenseIdx) => {
|
||||
let expenseGrandTotal = 0;
|
||||
|
||||
kandangExpense.realisasi?.forEach(
|
||||
@@ -320,7 +328,9 @@ const ExpenseRealizationContent = ({
|
||||
<td>{realisasiItem.nonstock.name}</td>
|
||||
<td>{realisasiItem.qty}</td>
|
||||
<td>{formatCurrency(realisasiItem.price)}</td>
|
||||
<td className='w-xs'>{realisasiItem.note ?? '-'}</td>
|
||||
<td className='w-xs'>
|
||||
{realisasiItem.notes ?? '-'}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
@@ -330,13 +340,17 @@ const ExpenseRealizationContent = ({
|
||||
<th colSpan={2} className='text-right'>
|
||||
Total Biaya Keseluruhan:
|
||||
</th>
|
||||
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th>
|
||||
<th colSpan={2}>
|
||||
{formatCurrency(expenseGrandTotal)}
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -273,7 +273,7 @@ const ExpenseRequestContent = ({
|
||||
<>
|
||||
<div>
|
||||
{initialValues && !isLoadingApprovalHistory && approvalHistory && (
|
||||
<div className='w-full max-w-5xl my-4 mx-auto'>
|
||||
<div className='w-full my-4 mx-auto'>
|
||||
<ApprovalSteps approvals={approvalHistory} />
|
||||
</div>
|
||||
)}
|
||||
@@ -281,7 +281,7 @@ const ExpenseRequestContent = ({
|
||||
<div className='w-full mt-4 flex flex-col gap-4'>
|
||||
{/* TODO: apply RBAC */}
|
||||
|
||||
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||
{isCurrentApprovalOnHeadArea && (
|
||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||
<Button
|
||||
@@ -414,7 +414,7 @@ const ExpenseRequestContent = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='overflow-x-auto w-full max-w-5xl mx-auto'>
|
||||
<div className='overflow-x-auto w-full mx-auto'>
|
||||
<table className='table table-sm table-zebra'>
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -608,7 +608,7 @@ const ExpenseRequestContent = ({
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
||||
<div className='w-full mt-8 mx-auto'>
|
||||
<h2 className='font-bold text-xl text-center'>
|
||||
Rincian Pengajuan Biaya Operasional
|
||||
</h2>
|
||||
@@ -654,7 +654,7 @@ const ExpenseRequestContent = ({
|
||||
<td>{pengajuanItem.qty}</td>
|
||||
<td>{formatCurrency(pengajuanItem.price)}</td>
|
||||
<td className='w-xs'>
|
||||
{pengajuanItem.note ?? '-'}
|
||||
{pengajuanItem.notes ?? '-'}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -54,17 +54,19 @@ const RowOptionsMenu = ({
|
||||
rejectClickHandler: () => void;
|
||||
deleteClickHandler: () => void;
|
||||
}) => {
|
||||
const showEditButton =
|
||||
props.row.original.latest_approval.step_number !== 6 &&
|
||||
const showEditButton = props.row.original.latest_approval
|
||||
? props.row.original.latest_approval.step_number !== 6 &&
|
||||
(props.row.original.latest_approval.step_number === 1 ||
|
||||
props.row.original.latest_approval.step_number === 2 ||
|
||||
props.row.original.latest_approval.step_number === 3 ||
|
||||
props.row.original.latest_approval.step_number === 4);
|
||||
props.row.original.latest_approval.step_number === 4)
|
||||
: false;
|
||||
|
||||
// TODO: apply RBAC
|
||||
const showRealizationButton =
|
||||
props.row.original.latest_approval.action !== 'REJECTED' &&
|
||||
props.row.original.latest_approval.step_number === 4;
|
||||
const showRealizationButton = props.row.original.latest_approval
|
||||
? props.row.original.latest_approval.action !== 'REJECTED' &&
|
||||
props.row.original.latest_approval.step_number === 4
|
||||
: false;
|
||||
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
@@ -278,6 +280,7 @@ const ExpensesTable = () => {
|
||||
cell: ({ row }) => {
|
||||
const isCheckboxDisabled =
|
||||
!row.getCanSelect() ||
|
||||
!row.original.latest_approval ||
|
||||
row.original.latest_approval.action === 'REJECTED';
|
||||
|
||||
return (
|
||||
@@ -413,6 +416,8 @@ const ExpensesTable = () => {
|
||||
const tableEnableRowSelectionHandler: (row: Row<Expense>) => boolean = (
|
||||
row
|
||||
) => {
|
||||
if (!row.original.latest_approval) return false;
|
||||
|
||||
return (
|
||||
row.original.latest_approval.action !== 'REJECTED' &&
|
||||
row.original.latest_approval.step_number !== 6
|
||||
@@ -692,14 +697,6 @@ const ExpensesTable = () => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Biaya Operasional'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-12 justify-end gap-2'>
|
||||
@@ -753,17 +750,12 @@ const ExpensesTable = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Baris'
|
||||
options={ROWS_OPTIONS}
|
||||
value={{
|
||||
label: String(tableFilterState.pageSize),
|
||||
value: tableFilterState.pageSize,
|
||||
}}
|
||||
onChange={pageSizeChangeHandler}
|
||||
className={{
|
||||
wrapper: 'col-span-12 max-w-28 justify-self-end',
|
||||
}}
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Biaya Operasional'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{ wrapper: 'col-span-12 max-w-52 justify-self-end' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { isResponseSuccess } from '@/lib/api-helper';
|
||||
interface ExpenseKandangsTableProps {
|
||||
locationId?: number;
|
||||
type: 'add' | 'edit' | 'detail';
|
||||
formType?: 'request' | 'realization';
|
||||
selectedKandangs: {
|
||||
id?: number;
|
||||
name?: string;
|
||||
@@ -31,6 +32,7 @@ interface ExpenseKandangsTableProps {
|
||||
|
||||
const ExpenseKandangsTable = ({
|
||||
type,
|
||||
formType = 'request',
|
||||
locationId,
|
||||
selectedKandangs,
|
||||
onChange,
|
||||
@@ -172,7 +174,16 @@ const ExpenseKandangsTable = ({
|
||||
updateSortingFilter('picSort', picSortFilter);
|
||||
}, [sorting, updateSortingFilter]);
|
||||
|
||||
// Tampilkan tabel jika:
|
||||
// 1. Mode request pertama kali (type='add' dan formType='request')
|
||||
// 2. Atau sudah ada kandang yang dipilih
|
||||
const shouldShowTable =
|
||||
(type === 'add' && formType === 'request') ||
|
||||
(selectedKandangs.length > 0 && selectedKandangs.some((k) => k.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowTable && (
|
||||
<Card
|
||||
className={{
|
||||
wrapper: className?.wrapper,
|
||||
@@ -184,7 +195,11 @@ const ExpenseKandangsTable = ({
|
||||
onOpenChange={setOpen}
|
||||
title={
|
||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||
<div className='card-title'>Pilih Kandang</div>
|
||||
<div className='card-title'>
|
||||
{formType === 'realization'
|
||||
? 'Kandang yang Direalisasikan'
|
||||
: 'Pilih Kandang'}
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
icon='material-symbols:keyboard-arrow-down'
|
||||
@@ -235,6 +250,8 @@ const ExpenseKandangsTable = ({
|
||||
/>
|
||||
</Collapse>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ export const getExpenseRealizationFormInitialValues = (
|
||||
? formatDate(initialValues?.realization_date, 'YYYY-MM-DD')
|
||||
: undefined,
|
||||
kandangs: initialValues?.kandangs.map((kandang) => ({
|
||||
id: kandang.kandang_id,
|
||||
id: kandang.id,
|
||||
name: kandang.name,
|
||||
})),
|
||||
supplier: initialValues?.supplier
|
||||
@@ -159,7 +159,7 @@ export const getExpenseRealizationFormInitialValues = (
|
||||
},
|
||||
quantity: realisasiItem.qty,
|
||||
price: realisasiItem.price,
|
||||
notes: realisasiItem.note,
|
||||
notes: realisasiItem.notes,
|
||||
};
|
||||
})
|
||||
: kandangExpense.pengajuans
|
||||
@@ -170,7 +170,7 @@ export const getExpenseRealizationFormInitialValues = (
|
||||
},
|
||||
quantity: expenseItem.qty,
|
||||
price: expenseItem.price,
|
||||
notes: expenseItem.note,
|
||||
notes: expenseItem.notes,
|
||||
}))
|
||||
: [];
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ const ExpenseRealizationForm = ({
|
||||
}, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]);
|
||||
|
||||
return (
|
||||
<section className='w-full max-w-5xl'>
|
||||
<section className='w-full'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/expense'
|
||||
@@ -297,6 +297,7 @@ const ExpenseRealizationForm = ({
|
||||
|
||||
<ExpenseKandangsTable
|
||||
type='detail'
|
||||
formType='realization'
|
||||
locationId={formik.values.location?.value}
|
||||
selectedKandangs={formik.values.kandangs ?? []}
|
||||
onChange={kandangsChangeHandler}
|
||||
|
||||
@@ -41,22 +41,25 @@ type ExpenseFormSchemaType = {
|
||||
export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
||||
Yup.object({
|
||||
category: Yup.object({
|
||||
value: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
|
||||
label: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
|
||||
value: Yup.string()
|
||||
.oneOf(['BOP', 'NON-BOP'])
|
||||
.required('Kategori wajib diisi!'),
|
||||
label: Yup.string()
|
||||
.oneOf(['BOP', 'NON-BOP'])
|
||||
.required('Kategori wajib diisi!'),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
.required('Kategori wajib diisi!')
|
||||
.typeError('Kategori wajib diisi!'),
|
||||
|
||||
location: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
}).nullable(),
|
||||
|
||||
location_id: Yup.number()
|
||||
.required('Lokasi wajib diisi!')
|
||||
.min(1, 'Lokasi wajib diisi!')
|
||||
.required('Lokasi wajib diisi!')
|
||||
.typeError('Lokasi wajib diisi!'),
|
||||
|
||||
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
|
||||
@@ -73,9 +76,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
||||
supplier: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
}).nullable(),
|
||||
|
||||
supplier_id: Yup.number()
|
||||
.required('Vendor wajib diisi!')
|
||||
@@ -104,9 +105,12 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
||||
.of(
|
||||
Yup.object({
|
||||
nonstock: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
value: Yup.number().min(1).required('Nonstock wajib diisi!'),
|
||||
label: Yup.string().required('Nonstock wajib diisi!'),
|
||||
})
|
||||
.nullable()
|
||||
.required('Nonstock wajib diisi!')
|
||||
.typeError('Nonstock wajib diisi!'),
|
||||
nonstock_id: Yup.number()
|
||||
.required('Nonstock wajib diisi!')
|
||||
.min(1, 'Nonstock wajib diisi!')
|
||||
@@ -204,7 +208,7 @@ export const getExpenseFormInitialValues = (
|
||||
nonstock_id: expenseItem.nonstock.id,
|
||||
quantity: expenseItem.qty,
|
||||
price: expenseItem.price,
|
||||
notes: expenseItem.note,
|
||||
notes: expenseItem.notes,
|
||||
}))
|
||||
: [],
|
||||
}))
|
||||
|
||||
@@ -190,30 +190,18 @@ const ExpenseRequestForm = ({
|
||||
formik.setFieldValue('category', val);
|
||||
};
|
||||
|
||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
const locationChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const location = val as OptionType | null;
|
||||
const locationId = location ? Number(location.value) : 0;
|
||||
|
||||
formik.setFieldTouched('location', true);
|
||||
formik.setFieldValue('location', val);
|
||||
|
||||
const locationId = Array.isArray(val) ? val[0]?.value : val?.value;
|
||||
formik.setFieldValue('location', location);
|
||||
formik.setFieldTouched('location_id', true);
|
||||
formik.setFieldValue('location_id', locationId);
|
||||
|
||||
formik.setFieldValue('kandangs', []);
|
||||
|
||||
// Auto-create expense item for location (without kandang)
|
||||
formik.setFieldValue('expense_nonstocks', [
|
||||
{
|
||||
cost_items: [
|
||||
{
|
||||
nonstock: null,
|
||||
nonstock_id: 0,
|
||||
quantity: undefined,
|
||||
price: undefined,
|
||||
notes: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
};
|
||||
[]
|
||||
);
|
||||
|
||||
const kandangsChangeHandler = (
|
||||
kandangs: { id?: number; name?: string }[]
|
||||
@@ -268,6 +256,7 @@ const ExpenseRequestForm = ({
|
||||
|
||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('supplier', true);
|
||||
formik.setFieldTouched('supplier_id', true);
|
||||
formik.setFieldValue('supplier', val);
|
||||
|
||||
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
|
||||
@@ -360,7 +349,7 @@ const ExpenseRequestForm = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full max-w-5xl'>
|
||||
<section className='w-full'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/expense'
|
||||
@@ -407,6 +396,16 @@ const ExpenseRequestForm = ({
|
||||
placeholder='Pilih Kategori'
|
||||
value={formik.values.category}
|
||||
onChange={categoryChangeHandler}
|
||||
isError={
|
||||
formik.touched.category && Boolean(formik.errors.category)
|
||||
}
|
||||
errorMessage={
|
||||
formik.touched.category && formik.errors.category
|
||||
? typeof formik.errors.category === 'object'
|
||||
? 'Kategori wajib diisi!'
|
||||
: (formik.errors.category as string)
|
||||
: undefined
|
||||
}
|
||||
options={[
|
||||
{
|
||||
value: 'BOP',
|
||||
@@ -427,8 +426,13 @@ const ExpenseRequestForm = ({
|
||||
value={formik.values.location}
|
||||
onChange={locationChangeHandler}
|
||||
options={locationOptions}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
onInputChange={setLocationInputValue}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
isError={
|
||||
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||
}
|
||||
errorMessage={formik.errors.location_id as string}
|
||||
isClearable
|
||||
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
||||
/>
|
||||
|
||||
@@ -438,6 +442,12 @@ const ExpenseRequestForm = ({
|
||||
required
|
||||
value={formik.values.transaction_date}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.transaction_date &&
|
||||
Boolean(formik.errors.transaction_date)
|
||||
}
|
||||
errorMessage={formik.errors.transaction_date as string}
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-4',
|
||||
}}
|
||||
@@ -460,8 +470,12 @@ const ExpenseRequestForm = ({
|
||||
value={formik.values.supplier}
|
||||
onChange={supplierChangeHandler}
|
||||
options={supplierOptions}
|
||||
isLoading={isLoadingVendorOptions}
|
||||
onInputChange={setVendorInputValue}
|
||||
isLoading={isLoadingVendorOptions}
|
||||
isError={
|
||||
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
|
||||
}
|
||||
errorMessage={formik.errors.supplier_id as string}
|
||||
className={{ wrapper: 'col-span-12' }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||
true
|
||||
);
|
||||
formik.setFieldTouched(
|
||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||
val
|
||||
@@ -96,7 +100,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
};
|
||||
|
||||
const isExpenseRepeaterInputError = (
|
||||
column: 'nonstock' | 'quantity' | 'price' | 'notes',
|
||||
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
||||
kandangExpenseIdx: number,
|
||||
expenseIdx: number
|
||||
) => {
|
||||
@@ -105,11 +109,14 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
expenseIdx
|
||||
]?.[column] &&
|
||||
Boolean(
|
||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof
|
||||
Object &&
|
||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx] &&
|
||||
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx] ===
|
||||
'object' &&
|
||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||
expenseIdx
|
||||
] instanceof Object &&
|
||||
] &&
|
||||
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx]
|
||||
.cost_items?.[expenseIdx] === 'object' &&
|
||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||
expenseIdx
|
||||
]?.[column]
|
||||
@@ -117,6 +124,32 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
);
|
||||
};
|
||||
|
||||
const getExpenseRepeaterErrorMessage = (
|
||||
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
||||
kandangExpenseIdx: number,
|
||||
expenseIdx: number
|
||||
): string => {
|
||||
const kandangError = formik.errors.expense_nonstocks?.[kandangExpenseIdx];
|
||||
|
||||
if (!kandangError || typeof kandangError !== 'object') return '';
|
||||
|
||||
if (!('cost_items' in kandangError)) return '';
|
||||
|
||||
const costItemsError = kandangError.cost_items?.[expenseIdx];
|
||||
|
||||
if (!costItemsError || typeof costItemsError !== 'object') return '';
|
||||
|
||||
const fieldError = costItemsError[column as keyof typeof costItemsError];
|
||||
|
||||
if (!fieldError) return '';
|
||||
|
||||
if (typeof fieldError === 'object' && fieldError !== null) {
|
||||
return 'Nonstock wajib diisi!';
|
||||
}
|
||||
|
||||
return String(fieldError);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={{
|
||||
@@ -202,10 +235,21 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
val
|
||||
);
|
||||
}}
|
||||
isError={isExpenseRepeaterInputError(
|
||||
'nonstock_id',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
errorMessage={getExpenseRepeaterErrorMessage(
|
||||
'nonstock_id',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
options={nonstockOptions}
|
||||
isLoading={isLoadingNonstockOptions}
|
||||
onInputChange={setNonstockInputValue}
|
||||
className={{ wrapper: 'min-w-48' }}
|
||||
isClearable={true}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -226,6 +270,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
errorMessage={getExpenseRepeaterErrorMessage(
|
||||
'quantity',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
className={{ wrapper: 'min-w-24' }}
|
||||
/>
|
||||
</td>
|
||||
@@ -246,6 +295,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
errorMessage={getExpenseRepeaterErrorMessage(
|
||||
'price',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
inputPrefix={
|
||||
<span className='text-gray-600 font-medium'>
|
||||
Rp
|
||||
@@ -271,6 +325,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
errorMessage={getExpenseRepeaterErrorMessage(
|
||||
'notes',
|
||||
kandangExpenseIdx,
|
||||
expenseIdx
|
||||
)}
|
||||
className={{ wrapper: 'min-w-24' }}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -447,7 +447,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{pengajuan.note}
|
||||
{pengajuan.notes}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -607,7 +607,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
||||
]}
|
||||
>
|
||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||
{realisasi.note}
|
||||
{realisasi.notes}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -34,7 +34,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
||||
},
|
||||
{
|
||||
label: 'Pihak',
|
||||
value: finance.party.id ? finance.party.name : '-',
|
||||
value: finance.party?.id ? finance.party?.name : '-',
|
||||
},
|
||||
{
|
||||
label: 'Tanggal',
|
||||
@@ -56,25 +56,21 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
||||
},
|
||||
{
|
||||
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)}`,
|
||||
value: finance.party.account_number,
|
||||
label: `Rekening ${formatTitleCase(finance.party?.type)}`,
|
||||
value: finance.party?.account_number,
|
||||
},
|
||||
{
|
||||
label: 'Nominal',
|
||||
value: formatCurrency(finance.expense_amount),
|
||||
},
|
||||
{
|
||||
label: 'Sisa',
|
||||
value: formatCurrency(finance.income_amount),
|
||||
value: formatCurrency(finance.nominal),
|
||||
},
|
||||
].filter((item) => {
|
||||
// Hide party account number row if transaction type is INJECTION
|
||||
if (
|
||||
FINANCE_INJECTION_STATUS.includes(finance.transaction_type) &&
|
||||
item.label === `Rekening ${formatTitleCase(finance.party.type)}`
|
||||
item.label === `Rekening ${formatTitleCase(finance.party?.type)}`
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -148,7 +144,8 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
||||
</Card>
|
||||
|
||||
<div className='flex flex-row gap-2 justify-end'>
|
||||
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && (
|
||||
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) &&
|
||||
finance.party?.type !== 'SUPPLIER' && (
|
||||
<RequirePermission permissions='lti.finance.payments.update'>
|
||||
<Button
|
||||
color='warning'
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
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 useSWR from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import Dropdown from '@/components/dropdown/Dropdown';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import Menu from '@/components/menu/Menu';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
import Table from '@/components/Table';
|
||||
import Tooltip from '@/components/Tooltip';
|
||||
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { Finance } from '@/types/api/finance/finance';
|
||||
@@ -23,7 +19,6 @@ import {
|
||||
FINANCE_INITIAL_BALANCE_STATUS,
|
||||
FINANCE_INJECTION_STATUS,
|
||||
FINANCE_TRANSACTION_STATUS,
|
||||
ROWS_OPTIONS,
|
||||
} from '@/config/constant';
|
||||
import { FinanceApi } from '@/services/api/finance';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
@@ -70,7 +65,8 @@ const RowOptionsMenu = ({
|
||||
|
||||
{FINANCE_TRANSACTION_STATUS.includes(
|
||||
props.row.original.transaction_type
|
||||
) && (
|
||||
) &&
|
||||
props.row.original.party?.type !== 'SUPPLIER' && (
|
||||
<RequirePermission permissions='lti.finance.payments.update'>
|
||||
<Button
|
||||
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
|
||||
@@ -78,7 +74,11 @@ const RowOptionsMenu = ({
|
||||
color='warning'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
@@ -199,35 +199,37 @@ const FinanceTable = () => {
|
||||
|
||||
// ===== Options =====
|
||||
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 [
|
||||
{ label: 'Customer', value: 'CUSTOMER' },
|
||||
{ label: 'Supplier', value: 'SUPPLIER' },
|
||||
];
|
||||
}, []);
|
||||
const {
|
||||
options: partyTypeOptions,
|
||||
isLoadingOptions: partyTypeIsLoadingOptions,
|
||||
setInputValue: partyTypeInputValue,
|
||||
loadMore: partyTypeLoadMore,
|
||||
} = useSelect(
|
||||
selectedTransactionType
|
||||
? selectedTransactionType.value === 'CUSTOMER'
|
||||
? CustomerApi.basePath
|
||||
: SupplierApi.basePath
|
||||
: '',
|
||||
'id',
|
||||
'name'
|
||||
);
|
||||
const sortByOptions = useMemo(() => {
|
||||
return [
|
||||
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
|
||||
{ label: 'Tanggal Dibuat', value: 'created_at' },
|
||||
];
|
||||
}, []);
|
||||
const { options: bankOptions, rawData: bankRawData } = useSelect<Bank>(
|
||||
BankApi.basePath,
|
||||
'id',
|
||||
'alias',
|
||||
'',
|
||||
{
|
||||
limit: 'limit',
|
||||
}
|
||||
);
|
||||
const {
|
||||
options: bankOptions,
|
||||
rawData: bankRawData,
|
||||
setInputValue: bankInputValue,
|
||||
loadMore: bankLoadMore,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'alias');
|
||||
|
||||
// ===== Handler =====
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
@@ -344,10 +346,10 @@ const FinanceTable = () => {
|
||||
},
|
||||
{
|
||||
header: 'Pihak',
|
||||
accessorFn: (finance: Finance) => finance.party.name,
|
||||
accessorFn: (finance: Finance) => finance.party?.name,
|
||||
cell: (props: CellContext<Finance, unknown>) => {
|
||||
if (props.row.original.party.id) {
|
||||
return <span>{props.row.original.party.name}</span>;
|
||||
if (props.row.original.party?.id) {
|
||||
return <span>{props.row.original.party?.name}</span>;
|
||||
}
|
||||
return <span>{'-'}</span>;
|
||||
},
|
||||
@@ -368,12 +370,12 @@ const FinanceTable = () => {
|
||||
{
|
||||
header: 'Bank',
|
||||
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)',
|
||||
accessorFn: (finance: Finance) =>
|
||||
formatCurrency(finance.expense_amount),
|
||||
formatCurrency(Math.abs(finance.expense_amount)),
|
||||
},
|
||||
{
|
||||
header: 'Pemasukan (Rp)',
|
||||
@@ -476,38 +478,49 @@ const FinanceTable = () => {
|
||||
<div className='grid grid-cols-4 gap-6'>
|
||||
<SelectInput
|
||||
options={transactionTypeOptions}
|
||||
label='Jenis Transaksi'
|
||||
label='Tipe Transaksi'
|
||||
value={selectedTransactionType}
|
||||
onChange={transactionTypeChangeHandler}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
options={partyTypeOptions}
|
||||
label={
|
||||
selectedTransactionType
|
||||
? selectedTransactionType.value === 'CUSTOMER'
|
||||
? 'Pelanggan'
|
||||
: 'Supplier'
|
||||
: 'Pihak'
|
||||
}
|
||||
value={selectedPartyType}
|
||||
onChange={partyTypeChangeHandler}
|
||||
onInputChange={partyTypeInputValue}
|
||||
onMenuScrollToBottom={partyTypeLoadMore}
|
||||
isLoading={partyTypeIsLoadingOptions}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
options={
|
||||
isResponseSuccess(bankRawData)
|
||||
? bankOptions.map((bank) => ({
|
||||
label:
|
||||
bankRawData.data.find((data) => data.id === bank.value)
|
||||
bankRawData.data.find((data) => data.id === bank?.value)
|
||||
?.alias +
|
||||
' - ' +
|
||||
bankRawData.data.find((data) => data.id === bank.value)
|
||||
bankRawData.data.find((data) => data.id === bank?.value)
|
||||
?.account_number +
|
||||
' - ' +
|
||||
bankRawData.data.find((data) => data.id === bank.value)
|
||||
bankRawData.data.find((data) => data.id === bank?.value)
|
||||
?.owner,
|
||||
value: bank.value,
|
||||
value: bank?.value,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
label='Bank'
|
||||
value={selectedBank}
|
||||
onChange={bankChangeHandler}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
options={partyTypeOptions}
|
||||
label='Pihak'
|
||||
value={selectedPartyType}
|
||||
onChange={partyTypeChangeHandler}
|
||||
onInputChange={bankInputValue}
|
||||
onMenuScrollToBottom={bankLoadMore}
|
||||
isClearable
|
||||
/>
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -32,8 +32,10 @@ import {
|
||||
import { Bank } from '@/types/api/master-data/bank';
|
||||
import { useFormik } from 'formik';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Alert from '@/components/Alert';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
interface FormFinanceAddProps {
|
||||
type?: 'add' | 'edit';
|
||||
@@ -51,18 +53,22 @@ const FormFinanceAdd = ({
|
||||
initialValues,
|
||||
}: FormFinanceAddProps) => {
|
||||
const router = useRouter();
|
||||
const [serverErrorMessage, setServerErrorMessage] = useState('');
|
||||
const [isSupplier, setIsSupplier] = useState(
|
||||
initialValues?.party?.type === 'SUPPLIER'
|
||||
);
|
||||
|
||||
// ===== Formik =====
|
||||
const formikInitialValues = useMemo((): FinanceFormValues => {
|
||||
return {
|
||||
party_type_option:
|
||||
FINANCE_PARTY_TYPE_OPTIONS.find(
|
||||
(option) => option.value === initialValues?.party.type
|
||||
(option) => option.value === initialValues?.party?.type
|
||||
) || null,
|
||||
party_id_option: initialValues?.party
|
||||
? {
|
||||
label: initialValues?.party.name || '',
|
||||
value: initialValues?.party.id || 0,
|
||||
label: initialValues?.party?.name || '',
|
||||
value: initialValues?.party?.id || 0,
|
||||
}
|
||||
: null,
|
||||
payment_date: initialValues?.payment_date || '',
|
||||
@@ -72,11 +78,11 @@ const FormFinanceAdd = ({
|
||||
) || null,
|
||||
bank_id_option: initialValues?.bank
|
||||
? {
|
||||
label: initialValues.bank.name,
|
||||
value: initialValues.bank.id,
|
||||
label: initialValues?.bank?.name,
|
||||
value: initialValues?.bank?.id,
|
||||
}
|
||||
: null,
|
||||
party_account_number: initialValues?.party.account_number || '',
|
||||
party_account_number: initialValues?.party?.account_number || '',
|
||||
reference_number: initialValues?.reference_number || '',
|
||||
nominal: initialValues?.nominal.toString() || '',
|
||||
notes: initialValues?.notes || '',
|
||||
@@ -113,20 +119,22 @@ const FormFinanceAdd = ({
|
||||
options: partyOptions,
|
||||
isLoadingOptions: isLoadingPartyOptions,
|
||||
rawData: partyRawData,
|
||||
setInputValue: setPartyInputValue,
|
||||
loadMore: loadMorePartyOptions,
|
||||
} = useSelect<PartyCommonProps>(
|
||||
formik.values.party_type_option?.value === 'CUSTOMER'
|
||||
? CustomerApi.basePath
|
||||
: SupplierApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'',
|
||||
{ limit: 'limit' }
|
||||
'name'
|
||||
);
|
||||
const {
|
||||
options: bankOptions,
|
||||
rawData: bankRawData,
|
||||
isLoadingOptions: isLoadingBankOptions,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
||||
setInputValue: setBankInputValue,
|
||||
loadMore: loadMoreBankOptions,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||
|
||||
// ===== Helper Functions =====
|
||||
const transformFormValuesToPayload = (
|
||||
@@ -151,6 +159,7 @@ const FormFinanceAdd = ({
|
||||
|
||||
if (isResponseError(response)) {
|
||||
toast.error(response.message);
|
||||
setServerErrorMessage(response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,6 +175,7 @@ const FormFinanceAdd = ({
|
||||
|
||||
if (isResponseError(response)) {
|
||||
toast.error(response.message);
|
||||
setServerErrorMessage(response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -205,6 +215,7 @@ const FormFinanceAdd = ({
|
||||
? formik.errors.party_type_option
|
||||
: ''
|
||||
}
|
||||
isDisabled={type === 'edit' || isSupplier}
|
||||
required
|
||||
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'}`}
|
||||
options={partyOptions}
|
||||
value={formik.values.party_id_option}
|
||||
onInputChange={setPartyInputValue}
|
||||
onMenuScrollToBottom={loadMorePartyOptions}
|
||||
onChange={(value) => {
|
||||
formik.setFieldValue('party_id_option', value);
|
||||
if (isResponseSuccess(partyRawData) && value) {
|
||||
@@ -241,7 +254,7 @@ const FormFinanceAdd = ({
|
||||
}
|
||||
required
|
||||
isClearable
|
||||
isDisabled={!formik.values.party_type_option?.value}
|
||||
isDisabled={!formik.values.party_type_option?.value || isSupplier}
|
||||
/>
|
||||
<DateInput
|
||||
label='Tanggal'
|
||||
@@ -259,6 +272,7 @@ const FormFinanceAdd = ({
|
||||
: ''
|
||||
}
|
||||
required
|
||||
disabled={isSupplier}
|
||||
/>
|
||||
<SelectInput
|
||||
label='Metode Pembayaran'
|
||||
@@ -280,6 +294,7 @@ const FormFinanceAdd = ({
|
||||
}
|
||||
required
|
||||
isClearable
|
||||
isDisabled={isSupplier}
|
||||
/>
|
||||
<SelectInput
|
||||
label='Bank'
|
||||
@@ -304,6 +319,8 @@ const FormFinanceAdd = ({
|
||||
: []
|
||||
}
|
||||
value={formik.values.bank_id_option}
|
||||
onInputChange={setBankInputValue}
|
||||
onMenuScrollToBottom={loadMoreBankOptions}
|
||||
onChange={(value) => {
|
||||
formik.setFieldValue('bank_id_option', value);
|
||||
}}
|
||||
@@ -318,6 +335,7 @@ const FormFinanceAdd = ({
|
||||
}
|
||||
required
|
||||
isClearable
|
||||
isDisabled={isSupplier}
|
||||
/>
|
||||
<TextInput
|
||||
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
|
||||
readOnly
|
||||
disabled={isSupplier}
|
||||
/>
|
||||
<TextInput
|
||||
label='Nomor Referensi'
|
||||
@@ -357,6 +376,7 @@ const FormFinanceAdd = ({
|
||||
: ''
|
||||
}
|
||||
required
|
||||
disabled={isSupplier}
|
||||
/>
|
||||
<NumberInput
|
||||
label='Nominal'
|
||||
@@ -372,6 +392,7 @@ const FormFinanceAdd = ({
|
||||
: ''
|
||||
}
|
||||
required
|
||||
disabled={isSupplier}
|
||||
/>
|
||||
<TextArea
|
||||
label='Catatan'
|
||||
@@ -387,8 +408,18 @@ const FormFinanceAdd = ({
|
||||
: ''
|
||||
}
|
||||
required
|
||||
disabled={isSupplier}
|
||||
/>
|
||||
<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'>
|
||||
<Button
|
||||
type='reset'
|
||||
|
||||
+1
-7
@@ -27,13 +27,7 @@ export const InitialBalanceFormSchema = Yup.object().shape({
|
||||
'Pihak wajib diisi',
|
||||
(value) => value !== null && value !== undefined
|
||||
),
|
||||
bank_id_option: Yup.mixed()
|
||||
.nullable()
|
||||
.test(
|
||||
'is-valid-option',
|
||||
'Bank wajib diisi',
|
||||
(value) => value !== null && value !== undefined
|
||||
),
|
||||
bank_id_option: Yup.mixed().nullable(),
|
||||
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
|
||||
initial_balance_type_option: Yup.mixed()
|
||||
.nullable()
|
||||
|
||||
@@ -29,8 +29,9 @@ import { Bank } from '@/types/api/master-data/bank';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Alert from '@/components/Alert';
|
||||
|
||||
interface FormFinanceAddInitialBalanceProps {
|
||||
type?: 'add' | 'edit';
|
||||
@@ -42,6 +43,7 @@ const FormFinanceAddInitialBalance = ({
|
||||
initialValues,
|
||||
}: FormFinanceAddInitialBalanceProps) => {
|
||||
const router = useRouter();
|
||||
const [serverErrorMessage, setServerErrorMessage] = useState('');
|
||||
|
||||
// ===== Formik =====
|
||||
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
|
||||
@@ -53,18 +55,18 @@ const FormFinanceAddInitialBalance = ({
|
||||
return {
|
||||
party_type_option:
|
||||
FINANCE_PARTY_TYPE_OPTIONS.find(
|
||||
(option) => option.value === initialValues?.party.type
|
||||
(option) => option.value === initialValues?.party?.type
|
||||
) || null,
|
||||
party_id_option: initialValues?.party
|
||||
? {
|
||||
label: initialValues.party.name,
|
||||
value: initialValues.party.id,
|
||||
label: initialValues.party?.name,
|
||||
value: initialValues.party?.id,
|
||||
}
|
||||
: null,
|
||||
bank_id_option: initialValues?.bank
|
||||
? {
|
||||
label: initialValues.bank.name,
|
||||
value: initialValues.bank.id,
|
||||
label: initialValues.bank?.name,
|
||||
value: initialValues.bank?.id,
|
||||
}
|
||||
: null,
|
||||
reference_number: initialValues?.reference_number || '',
|
||||
@@ -104,21 +106,25 @@ const FormFinanceAddInitialBalance = ({
|
||||
});
|
||||
|
||||
// ===== Options =====
|
||||
const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } =
|
||||
useSelect(
|
||||
const {
|
||||
options: partyOptions,
|
||||
isLoadingOptions: isLoadingPartyOptions,
|
||||
setInputValue: setPartyInputValue,
|
||||
loadMore: loadMorePartyOptions,
|
||||
} = useSelect(
|
||||
formik.values.party_type_option?.value === 'CUSTOMER'
|
||||
? CustomerApi.basePath
|
||||
: SupplierApi.basePath,
|
||||
'id',
|
||||
'name',
|
||||
'',
|
||||
{ limit: 'limit' }
|
||||
'name'
|
||||
);
|
||||
const {
|
||||
options: bankOptions,
|
||||
rawData: bankRawData,
|
||||
isLoadingOptions: isLoadingBankOptions,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
||||
setInputValue: setBankInputValue,
|
||||
loadMore: loadMoreBankOptions,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||
|
||||
// ===== Helper Functions =====
|
||||
const transformFormValuesToPayload = (
|
||||
@@ -143,6 +149,7 @@ const FormFinanceAddInitialBalance = ({
|
||||
|
||||
if (isResponseError(response)) {
|
||||
toast.error(response.message);
|
||||
setServerErrorMessage(response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,6 +169,7 @@ const FormFinanceAddInitialBalance = ({
|
||||
|
||||
if (isResponseError(response)) {
|
||||
toast.error(response.message);
|
||||
setServerErrorMessage(response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,6 +197,8 @@ const FormFinanceAddInitialBalance = ({
|
||||
placeholder='Pilih jenis pihak'
|
||||
options={FINANCE_PARTY_TYPE_OPTIONS}
|
||||
value={formik.values.party_type_option}
|
||||
onInputChange={setPartyInputValue}
|
||||
onMenuScrollToBottom={loadMorePartyOptions}
|
||||
onChange={(value) => {
|
||||
formik.setFieldValue('party_type_option', value);
|
||||
formik.setFieldValue('party_id_option', null);
|
||||
@@ -205,6 +215,7 @@ const FormFinanceAddInitialBalance = ({
|
||||
: ''
|
||||
}
|
||||
required
|
||||
isDisabled={type === 'edit'}
|
||||
isClearable
|
||||
/>
|
||||
<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'}`}
|
||||
options={partyOptions}
|
||||
value={formik.values.party_id_option}
|
||||
onInputChange={setPartyInputValue}
|
||||
onMenuScrollToBottom={loadMorePartyOptions}
|
||||
onChange={(value) => {
|
||||
formik.setFieldValue('party_id_option', value);
|
||||
}}
|
||||
@@ -269,7 +282,6 @@ const FormFinanceAddInitialBalance = ({
|
||||
? formik.errors.bank_id_option
|
||||
: ''
|
||||
}
|
||||
required
|
||||
isClearable
|
||||
/>
|
||||
<TextInput
|
||||
@@ -354,7 +366,18 @@ const FormFinanceAddInitialBalance = ({
|
||||
}
|
||||
required
|
||||
/>
|
||||
|
||||
<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'>
|
||||
<Button
|
||||
type='reset'
|
||||
|
||||
@@ -24,8 +24,10 @@ import {
|
||||
import { Bank } from '@/types/api/master-data/bank';
|
||||
import { useFormik } from 'formik';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Alert from '@/components/Alert';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
interface FormFinanceInjectionProps {
|
||||
type?: 'add' | 'edit';
|
||||
@@ -37,14 +39,15 @@ const FormFinanceInjection = ({
|
||||
initialValues,
|
||||
}: FormFinanceInjectionProps) => {
|
||||
const router = useRouter();
|
||||
const [serverErrorMessage, setServerErrorMessage] = useState('');
|
||||
|
||||
// ===== Formik =====
|
||||
const formikInitialValues = useMemo((): InjectionFormValues => {
|
||||
return {
|
||||
bank_id_option: initialValues?.bank
|
||||
? {
|
||||
label: initialValues.bank.name,
|
||||
value: initialValues.bank.id,
|
||||
label: initialValues.bank?.name,
|
||||
value: initialValues.bank?.id,
|
||||
}
|
||||
: null,
|
||||
adjustment_date: initialValues?.payment_date || '',
|
||||
@@ -80,7 +83,9 @@ const FormFinanceInjection = ({
|
||||
options: bankOptions,
|
||||
rawData: bankRawData,
|
||||
isLoadingOptions: isLoadingBankOptions,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
||||
setInputValue: setBankInputValue,
|
||||
loadMore: loadMoreBankOptions,
|
||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||
|
||||
// ===== Helper Functions =====
|
||||
const transformFormValuesToPayload = (
|
||||
@@ -101,6 +106,7 @@ const FormFinanceInjection = ({
|
||||
|
||||
if (isResponseError(response)) {
|
||||
toast.error(response.message);
|
||||
setServerErrorMessage(response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -117,6 +123,7 @@ const FormFinanceInjection = ({
|
||||
|
||||
if (isResponseError(response)) {
|
||||
toast.error(response.message);
|
||||
setServerErrorMessage(response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,6 +169,8 @@ const FormFinanceInjection = ({
|
||||
: []
|
||||
}
|
||||
value={formik.values.bank_id_option}
|
||||
onInputChange={setBankInputValue}
|
||||
onMenuScrollToBottom={loadMoreBankOptions}
|
||||
onChange={(value) => {
|
||||
formik.setFieldValue('bank_id_option', value);
|
||||
}}
|
||||
@@ -226,6 +235,15 @@ const FormFinanceInjection = ({
|
||||
required
|
||||
/>
|
||||
<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'>
|
||||
<Button
|
||||
type='reset'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||
import {
|
||||
CreateInventoryAdjustmentPayload,
|
||||
@@ -22,12 +22,18 @@ import {
|
||||
} from '@/services/api/master-data';
|
||||
import Button from '@/components/Button';
|
||||
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 { RadioGroup } from '@/components/input/RadioInput';
|
||||
import TextArea from '@/components/input/TextArea';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
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 {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -44,10 +50,7 @@ const InventoryAdjustmentForm = ({
|
||||
InventoryAdjustmentFormErrorMessage,
|
||||
setInventoryAdjustmentFormErrorMessage,
|
||||
] = useState('');
|
||||
const [selectedProductCategories, setSelectedProductCategories] =
|
||||
useState('');
|
||||
const [disabledProduct, setDisabledProduct] = useState(true);
|
||||
const [optionsProduct, setOptionsProduct] = useState<OptionType[]>([]);
|
||||
const [quantityLabel, setQuantityLabel] = useState('Tambah Stok');
|
||||
|
||||
// Submit Handler
|
||||
@@ -108,45 +111,30 @@ const InventoryAdjustmentForm = ({
|
||||
});
|
||||
|
||||
// Fetch Data
|
||||
const productCategoriesUrl = `${
|
||||
ProductCategoryApi.basePath
|
||||
}?${new URLSearchParams({
|
||||
search: '',
|
||||
}).toString()}`;
|
||||
const { data: productCategories, isLoading: isLoadingProductCategories } =
|
||||
useSWR(productCategoriesUrl, ProductCategoryApi.getAllFetcher);
|
||||
const {
|
||||
setInputValue: setProductCategoryInputValue,
|
||||
options: productCategoryOptions,
|
||||
isLoadingOptions: isLoadingProductCategoryOptions,
|
||||
loadMore: loadMoreProductCategories,
|
||||
} = useSelect<ProductCategory>(ProductCategoryApi.basePath, 'id', 'name');
|
||||
|
||||
const productUrl = `${ProductApi.basePath}?${new URLSearchParams({
|
||||
search: '',
|
||||
product_category_id: selectedProductCategories,
|
||||
}).toString()}`;
|
||||
const { data: products, isLoading: isLoadingProducts } = useSWR(
|
||||
productUrl,
|
||||
ProductApi.getAllFetcher
|
||||
);
|
||||
const {
|
||||
setInputValue: setProductInputValue,
|
||||
options: productOptions,
|
||||
isLoadingOptions: isLoadingProductOptions,
|
||||
loadMore: loadMoreProducts,
|
||||
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
|
||||
product_category_id: formik.values.product_category_id
|
||||
? String(formik.values.product_category_id)
|
||||
: '',
|
||||
});
|
||||
|
||||
const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({
|
||||
search: '',
|
||||
limit: '100',
|
||||
}).toString()}`;
|
||||
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
|
||||
warehouseUrl,
|
||||
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,
|
||||
}))
|
||||
: [];
|
||||
const {
|
||||
setInputValue: setWarehouseInputValue,
|
||||
options: warehouseOptions,
|
||||
isLoadingOptions: isLoadingWarehouseOptions,
|
||||
loadMore: loadMoreWarehouses,
|
||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
||||
|
||||
// Options Handler
|
||||
const productCategoryChangeHandler = (
|
||||
@@ -157,7 +145,6 @@ const InventoryAdjustmentForm = ({
|
||||
|
||||
formik.setFieldValue('product_category', val);
|
||||
|
||||
setSelectedProductCategories((val as OptionType)?.value as string);
|
||||
const disabled = (val as OptionType)?.value == null;
|
||||
setDisabledProduct(disabled);
|
||||
formik.setFieldValue('product_id', 0);
|
||||
@@ -193,9 +180,6 @@ const InventoryAdjustmentForm = ({
|
||||
// Effect
|
||||
useEffect(() => {
|
||||
if (initialValues?.product_warehouse?.product?.id) {
|
||||
setSelectedProductCategories(
|
||||
String(initialValues.product_warehouse.product.id)
|
||||
);
|
||||
setDisabledProduct(false);
|
||||
formik.setFieldValue(
|
||||
'product_id',
|
||||
@@ -219,25 +203,10 @@ const InventoryAdjustmentForm = ({
|
||||
);
|
||||
formik.setFieldValue('note', initialValues.note);
|
||||
}
|
||||
}, [
|
||||
formik,
|
||||
initialValues,
|
||||
setQuantityLabel,
|
||||
setDisabledProduct,
|
||||
setSelectedProductCategories,
|
||||
]);
|
||||
}, [formik, initialValues, setQuantityLabel, setDisabledProduct]);
|
||||
useEffect(() => {
|
||||
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
|
||||
}, [formikSetValues, formikInitialValues]);
|
||||
useEffect(() => {
|
||||
if (isResponseSuccess(products)) {
|
||||
const options = products.data.map((p) => ({
|
||||
value: p.id,
|
||||
label: p.name,
|
||||
}));
|
||||
setOptionsProduct(options);
|
||||
}
|
||||
}, [products]);
|
||||
|
||||
// Utils Function
|
||||
const formatNumber = (value: string) => {
|
||||
@@ -282,9 +251,10 @@ const InventoryAdjustmentForm = ({
|
||||
label='Kategori Produk'
|
||||
value={formik.values.product_category as OptionType}
|
||||
onChange={productCategoryChangeHandler}
|
||||
onInputChange={setSelectedProductCategories}
|
||||
options={optionsProductCategory}
|
||||
isLoading={isLoadingProductCategories}
|
||||
onInputChange={setProductCategoryInputValue}
|
||||
options={productCategoryOptions}
|
||||
onMenuScrollToBottom={loadMoreProductCategories}
|
||||
isLoading={isLoadingProductCategoryOptions}
|
||||
isError={
|
||||
formik.touched.product_category &&
|
||||
Boolean(formik.errors.product_category)
|
||||
@@ -300,8 +270,10 @@ const InventoryAdjustmentForm = ({
|
||||
label='Produk'
|
||||
value={formik.values.product as OptionType}
|
||||
onChange={productChangeHandler}
|
||||
options={optionsProduct}
|
||||
isLoading={isLoadingProducts}
|
||||
onInputChange={setProductInputValue}
|
||||
options={productOptions}
|
||||
onMenuScrollToBottom={loadMoreProducts}
|
||||
isLoading={isLoadingProductOptions}
|
||||
isError={formik.touched.product && Boolean(formik.errors.product)}
|
||||
errorMessage={formik.errors.product as string}
|
||||
isDisabled={type === 'detail' || disabledProduct}
|
||||
@@ -314,8 +286,10 @@ const InventoryAdjustmentForm = ({
|
||||
label='Warehouse'
|
||||
value={formik.values.warehouse as OptionType}
|
||||
onChange={warehouseChangeHandler}
|
||||
options={optionsWarehouse}
|
||||
isLoading={isLoadingWarehouses}
|
||||
onInputChange={setWarehouseInputValue}
|
||||
options={warehouseOptions}
|
||||
onMenuScrollToBottom={loadMoreWarehouses}
|
||||
isLoading={isLoadingWarehouseOptions}
|
||||
isError={
|
||||
formik.touched.warehouse && Boolean(formik.errors.warehouse)
|
||||
}
|
||||
|
||||
@@ -110,6 +110,14 @@ const DeliveryProductObjectSchema = Yup.object({
|
||||
.typeError('Qty harus berupa angka!'),
|
||||
});
|
||||
|
||||
const DeliveryDocumentSchema = Yup.mixed<File | MovementDocument>()
|
||||
.nullable()
|
||||
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value): boolean => {
|
||||
if (!value) return true;
|
||||
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
||||
return true;
|
||||
});
|
||||
|
||||
const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
||||
delivery_cost: Yup.number()
|
||||
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
||||
@@ -135,13 +143,7 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
||||
}),
|
||||
document_path: Yup.string().nullable().optional(),
|
||||
document_index: Yup.number().optional(),
|
||||
document: Yup.mixed<File | MovementDocument>()
|
||||
.nullable()
|
||||
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
|
||||
if (!value) return true;
|
||||
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
||||
return true;
|
||||
}),
|
||||
document: DeliveryDocumentSchema,
|
||||
driver_name: Yup.string().required('Nama sopir wajib diisi!'),
|
||||
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
|
||||
supplier: Yup.object({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import useSWR from 'swr';
|
||||
|
||||
@@ -38,6 +38,8 @@ import Card from '@/components/Card';
|
||||
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||
import { getUniqueFormikErrors } from '@/lib/formik-helper';
|
||||
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 {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -49,10 +51,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
|
||||
const [
|
||||
productWarehouseSelectInputValue,
|
||||
setProductWarehouseSelectInputValue,
|
||||
] = useState('');
|
||||
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
||||
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
|
||||
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
||||
@@ -93,10 +91,13 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
|
||||
// ===== USE SELECT HOOKS =====
|
||||
const {
|
||||
inputValue: warehouseSelectInputValue,
|
||||
setInputValue: setWarehouseSelectInputValue,
|
||||
isLoadingOptions: isLoadingWarehouses,
|
||||
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
|
||||
loadMore: loadMoreWarehouses,
|
||||
rawData: warehouses,
|
||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search', {
|
||||
flag: 'EKSPEDISI',
|
||||
});
|
||||
|
||||
// ===== SELECT INPUT DATA =====
|
||||
const {
|
||||
@@ -107,12 +108,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
category: 'BOP',
|
||||
});
|
||||
|
||||
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
||||
const { data: warehouses } = useSWR(
|
||||
warehousesUrl,
|
||||
WarehouseApi.getAllFetcher
|
||||
);
|
||||
|
||||
// ===== DATA PROCESSING =====
|
||||
const warehouseStockMap = useMemo(() => {
|
||||
if (!isResponseSuccess(allProductWarehouses)) return new Map();
|
||||
@@ -268,25 +263,63 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||
const getProductWarehousesUrl = useCallback(() => {
|
||||
const productWarehouseParams = new URLSearchParams({
|
||||
search: productWarehouseSelectInputValue,
|
||||
});
|
||||
if (formik.values.source_warehouse_id) {
|
||||
productWarehouseParams.append(
|
||||
'warehouse_id',
|
||||
formik.values.source_warehouse_id.toString()
|
||||
const prevSourceWarehouseIdRef = useRef<number | null>(
|
||||
formik.values.source_warehouse_id
|
||||
);
|
||||
}
|
||||
return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
|
||||
}, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]);
|
||||
|
||||
const productWarehousesUrl = getProductWarehousesUrl();
|
||||
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
|
||||
useSWR(
|
||||
formik.values.source_warehouse_id ? productWarehousesUrl : null,
|
||||
ProductWarehouseApi.getAllFetcher
|
||||
// ===== RESET PRODUCTS WHEN SOURCE WAREHOUSE CHANGES =====
|
||||
useEffect(() => {
|
||||
const prevSourceWarehouseId = prevSourceWarehouseIdRef.current;
|
||||
const currentSourceWarehouseId = formik.values.source_warehouse_id;
|
||||
|
||||
if (
|
||||
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
||||
prevSourceWarehouseId !== null
|
||||
) {
|
||||
formik.setFieldValue('products', [
|
||||
{
|
||||
product: null,
|
||||
product_id: 0,
|
||||
product_qty: '',
|
||||
},
|
||||
]);
|
||||
formik.setFieldTouched('products', false);
|
||||
|
||||
const updatedDeliveries = formik.values.deliveries.map(
|
||||
(delivery: DeliverySchema) => ({
|
||||
...delivery,
|
||||
products: [
|
||||
{
|
||||
product: null,
|
||||
product_id: 0,
|
||||
product_qty: '',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
formik.setFieldTouched('deliveries', false);
|
||||
}
|
||||
|
||||
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
||||
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
|
||||
|
||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||
const {
|
||||
setInputValue: setProductWarehouseSelectInputValue,
|
||||
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)
|
||||
@@ -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 =====
|
||||
// Product Handlers
|
||||
const addProduct = () => {
|
||||
const handleTransferDateChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
formik.setFieldValue('transfer_date', e.target.value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSourceWarehouseChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const newSourceWarehouseId = (val as WarehouseOptionType)?.value;
|
||||
|
||||
if (
|
||||
newSourceWarehouseId &&
|
||||
newSourceWarehouseId === formik.values.destination_warehouse_id
|
||||
) {
|
||||
const destinationWarehouseName =
|
||||
(formik.values.destination_warehouse as WarehouseOptionType)?.label ||
|
||||
'gudang tujuan';
|
||||
toast.error(
|
||||
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
formik.setFieldTouched('source_warehouse', true);
|
||||
formik.setFieldValue('source_warehouse', val);
|
||||
formik.setFieldTouched('source_warehouse_id', true);
|
||||
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
||||
},
|
||||
[
|
||||
formik.values.destination_warehouse_id,
|
||||
formik.values.destination_warehouse,
|
||||
]
|
||||
);
|
||||
|
||||
const handleDestinationWarehouseChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const newDestinationWarehouseId = (val as WarehouseOptionType)?.value;
|
||||
|
||||
if (
|
||||
newDestinationWarehouseId &&
|
||||
newDestinationWarehouseId === formik.values.source_warehouse_id
|
||||
) {
|
||||
const sourceWarehouseName =
|
||||
(formik.values.source_warehouse as WarehouseOptionType)?.label ||
|
||||
'gudang asal';
|
||||
toast.error(
|
||||
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
formik.setFieldTouched('destination_warehouse', true);
|
||||
formik.setFieldValue('destination_warehouse', val);
|
||||
formik.setFieldTouched('destination_warehouse_id', true);
|
||||
formik.setFieldValue(
|
||||
'destination_warehouse_id',
|
||||
newDestinationWarehouseId
|
||||
);
|
||||
},
|
||||
[formik.values.source_warehouse_id, formik.values.source_warehouse]
|
||||
);
|
||||
|
||||
const addProduct = useCallback(() => {
|
||||
const newProducts = [
|
||||
...(formik.values.products || []),
|
||||
{
|
||||
@@ -373,10 +464,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
},
|
||||
];
|
||||
formik.setFieldValue('products', newProducts);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const removeProduct = useCallback(
|
||||
(i: number) => {
|
||||
const removeProduct = useCallback((i: number) => {
|
||||
const updatedProducts =
|
||||
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
||||
if (index !== i) {
|
||||
@@ -386,9 +476,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
}, []) ?? [];
|
||||
|
||||
formik.setFieldValue('products', updatedProducts);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const bulkRemoveProduct = useCallback(() => {
|
||||
const updatedProducts =
|
||||
@@ -397,10 +485,45 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
) ?? [];
|
||||
formik.setFieldValue('products', updatedProducts);
|
||||
setSelectedProducts([]);
|
||||
}, [formik, selectedProducts]);
|
||||
}, [formik, selectedProducts, setSelectedProducts]);
|
||||
|
||||
// Delivery Handlers
|
||||
const addDelivery = () => {
|
||||
const handleProductChange = useCallback(
|
||||
(idx: number, val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched(`products.${idx}.product`, true);
|
||||
formik.setFieldValue(`products.${idx}.product`, val);
|
||||
formik.setFieldTouched(`products.${idx}.product_id`, true);
|
||||
formik.setFieldValue(
|
||||
`products.${idx}.product_id`,
|
||||
(val as ProductWarehouseOptionType)?.value
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleProductSelectAllChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedProducts(formik.values.products?.map((_, idx) => idx) ?? []);
|
||||
} else {
|
||||
setSelectedProducts([]);
|
||||
}
|
||||
},
|
||||
[formik.values.products, setSelectedProducts]
|
||||
);
|
||||
|
||||
const handleProductCheckboxChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const idx = Number(e.target.name.replace('product-', ''));
|
||||
if (e.target.checked) {
|
||||
setSelectedProducts((prev) => [...prev, idx]);
|
||||
} else {
|
||||
setSelectedProducts((prev) => prev.filter((i) => i !== idx));
|
||||
}
|
||||
},
|
||||
[setSelectedProducts]
|
||||
);
|
||||
|
||||
const addDelivery = useCallback(() => {
|
||||
formik.setFieldValue('deliveries', [
|
||||
...(formik.values.deliveries || []),
|
||||
{
|
||||
@@ -420,25 +543,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
],
|
||||
},
|
||||
]);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const removeDelivery = useCallback(
|
||||
(i: number) => {
|
||||
const removeDelivery = useCallback((i: number) => {
|
||||
const updatedDeliveries =
|
||||
formik.values.deliveries?.reduce(
|
||||
(acc: DeliverySchema[], item, index) => {
|
||||
formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => {
|
||||
if (index !== i) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
) ?? [];
|
||||
}, []) ?? [];
|
||||
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const bulkRemoveDelivery = useCallback(() => {
|
||||
const updatedDeliveries =
|
||||
@@ -447,11 +564,81 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
) ?? [];
|
||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||
setSelectedDeliveries([]);
|
||||
}, [formik, selectedDeliveries]);
|
||||
}, [formik, selectedDeliveries, setSelectedDeliveries]);
|
||||
|
||||
// Cost Calculation Handlers
|
||||
const handleDeliveryCostChange = useCallback(
|
||||
(idx: number, value: number) => {
|
||||
const handleDeliverySelectAllChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedDeliveries(
|
||||
formik.values.deliveries?.map((_, idx) => idx) ?? []
|
||||
);
|
||||
} else {
|
||||
setSelectedDeliveries([]);
|
||||
}
|
||||
},
|
||||
[formik.values.deliveries, setSelectedDeliveries]
|
||||
);
|
||||
|
||||
const handleDeliveryCheckboxChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const idx = Number(e.target.name.replace('delivery-', ''));
|
||||
if (e.target.checked) {
|
||||
setSelectedDeliveries((prev) => [...prev, idx]);
|
||||
} else {
|
||||
setSelectedDeliveries((prev) => prev.filter((i) => i !== idx));
|
||||
}
|
||||
},
|
||||
[setSelectedDeliveries]
|
||||
);
|
||||
|
||||
const handleDeliveryProductChange = useCallback(
|
||||
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched(
|
||||
`deliveries.${deliveryIdx}.products.0.product`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
|
||||
formik.setFieldTouched(
|
||||
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||
(val as OptionType)?.value
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeliverySupplierChange = useCallback(
|
||||
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
|
||||
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
|
||||
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
|
||||
formik.setFieldValue(
|
||||
`deliveries.${deliveryIdx}.supplier_id`,
|
||||
(val as OptionType)?.value
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeliveryDocumentChange = useCallback(
|
||||
(deliveryIdx: number, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error('Ukuran dokumen maksimal 5 MB!');
|
||||
e.target.value = '';
|
||||
return;
|
||||
}
|
||||
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeliveryCostChange = useCallback((idx: number, value: number) => {
|
||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
||||
|
||||
const delivery = formik.values.deliveries?.[idx];
|
||||
@@ -470,9 +657,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleDeliveryCostPerItemChange = useCallback(
|
||||
(idx: number, value: number) => {
|
||||
@@ -492,7 +677,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[formik]
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeliveryCostChangeWrapper = useCallback(
|
||||
@@ -967,45 +1152,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
label='Gudang'
|
||||
placeholder='Pilih gudang asal...'
|
||||
value={formik.values.source_warehouse}
|
||||
onChange={(val) => {
|
||||
const newSourceWarehouseId = (val as WarehouseOptionType)
|
||||
?.value;
|
||||
|
||||
if (newSourceWarehouseId) {
|
||||
if (
|
||||
newSourceWarehouseId ===
|
||||
formik.values.destination_warehouse_id
|
||||
) {
|
||||
const destinationWarehouseName =
|
||||
(
|
||||
formik.values
|
||||
.destination_warehouse as WarehouseOptionType
|
||||
)?.label || 'gudang tujuan';
|
||||
|
||||
toast.error(
|
||||
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
formik.setFieldTouched('source_warehouse', true);
|
||||
formik.setFieldValue('source_warehouse', val);
|
||||
formik.setFieldTouched('source_warehouse_id', true);
|
||||
formik.setFieldValue(
|
||||
'source_warehouse_id',
|
||||
newSourceWarehouseId
|
||||
);
|
||||
|
||||
if (
|
||||
formik.errors.destination_warehouse_id ===
|
||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||
) {
|
||||
formik.setFieldError('destination_warehouse_id', undefined);
|
||||
}
|
||||
}}
|
||||
onChange={handleSourceWarehouseChange}
|
||||
options={warehouseOptions}
|
||||
onInputChange={setWarehouseSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreWarehouses}
|
||||
isLoading={isLoadingWarehouses}
|
||||
isError={
|
||||
formik.touched.source_warehouse_id &&
|
||||
@@ -1066,44 +1216,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
label='Gudang'
|
||||
placeholder='Pilih gudang tujuan...'
|
||||
value={formik.values.destination_warehouse}
|
||||
onChange={(val) => {
|
||||
const newDestinationWarehouseId = (val as WarehouseOptionType)
|
||||
?.value;
|
||||
|
||||
if (newDestinationWarehouseId) {
|
||||
if (
|
||||
newDestinationWarehouseId ===
|
||||
formik.values.source_warehouse_id
|
||||
) {
|
||||
const sourceWarehouseName =
|
||||
(formik.values.source_warehouse as WarehouseOptionType)
|
||||
?.label || 'gudang asal';
|
||||
|
||||
toast.error(
|
||||
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
formik.setFieldTouched('destination_warehouse', true);
|
||||
formik.setFieldValue('destination_warehouse', val);
|
||||
formik.setFieldTouched('destination_warehouse_id', true);
|
||||
formik.setFieldValue(
|
||||
'destination_warehouse_id',
|
||||
newDestinationWarehouseId
|
||||
);
|
||||
|
||||
if (
|
||||
formik.errors.destination_warehouse_id ===
|
||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||
) {
|
||||
formik.setFieldError('destination_warehouse_id', undefined);
|
||||
}
|
||||
}}
|
||||
onChange={handleDestinationWarehouseChange}
|
||||
options={warehouseOptions}
|
||||
onInputChange={setWarehouseSelectInputValue}
|
||||
isLoading={isLoadingWarehouses}
|
||||
onMenuScrollToBottom={loadMoreWarehouses}
|
||||
isError={
|
||||
formik.touched.destination_warehouse_id &&
|
||||
Boolean(formik.errors.destination_warehouse_id)
|
||||
@@ -1173,18 +1290,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
selectedProducts.length &&
|
||||
formik.values.products?.length > 0
|
||||
}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedProducts(
|
||||
formik.values.products?.map((_, idx) => idx) ??
|
||||
[]
|
||||
);
|
||||
} else {
|
||||
setSelectedProducts([]);
|
||||
}
|
||||
}}
|
||||
onChange={handleProductSelectAllChange}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -1221,17 +1327,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
<CheckboxInput
|
||||
name={`product-${idx}`}
|
||||
checked={selectedProducts.includes(idx)}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedProducts([...selectedProducts, idx]);
|
||||
} else {
|
||||
setSelectedProducts(
|
||||
selectedProducts.filter((i) => i !== idx)
|
||||
);
|
||||
}
|
||||
}}
|
||||
onChange={handleProductCheckboxChange}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -1243,26 +1339,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
<SelectInput
|
||||
required
|
||||
value={product.product ?? undefined}
|
||||
onChange={(val) => {
|
||||
formik.setFieldTouched(
|
||||
`products.${idx}.product`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`products.${idx}.product`,
|
||||
val
|
||||
);
|
||||
formik.setFieldTouched(
|
||||
`products.${idx}.product_id`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`products.${idx}.product_id`,
|
||||
(val as ProductWarehouseOptionType)?.value
|
||||
);
|
||||
}}
|
||||
onChange={(val) => handleProductChange(idx, val)}
|
||||
options={productWarehouseOptions}
|
||||
onInputChange={setProductWarehouseSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreProductWarehouses}
|
||||
isLoading={isLoadingProductWarehouses}
|
||||
isDisabled={
|
||||
type === 'detail' ||
|
||||
@@ -1386,19 +1466,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
selectedDeliveries.length &&
|
||||
formik.values.deliveries?.length > 0
|
||||
}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedDeliveries(
|
||||
formik.values.deliveries?.map(
|
||||
(_, idx) => idx
|
||||
) ?? []
|
||||
);
|
||||
} else {
|
||||
setSelectedDeliveries([]);
|
||||
}
|
||||
}}
|
||||
onChange={handleDeliverySelectAllChange}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -1481,20 +1549,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
<CheckboxInput
|
||||
name={`delivery-${idx}`}
|
||||
checked={selectedDeliveries.includes(idx)}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedDeliveries([
|
||||
...selectedDeliveries,
|
||||
idx,
|
||||
]);
|
||||
} else {
|
||||
setSelectedDeliveries(
|
||||
selectedDeliveries.filter((i) => i !== idx)
|
||||
);
|
||||
}
|
||||
}}
|
||||
onChange={handleDeliveryCheckboxChange}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
@@ -1507,24 +1562,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
required
|
||||
placeholder='Pilih produk...'
|
||||
value={delivery.products[0]?.product ?? undefined}
|
||||
onChange={(val) => {
|
||||
formik.setFieldTouched(
|
||||
`deliveries.${idx}.products.0.product`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`deliveries.${idx}.products.0.product`,
|
||||
val
|
||||
);
|
||||
formik.setFieldTouched(
|
||||
`deliveries.${idx}.products.0.product_id`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`deliveries.${idx}.products.0.product_id`,
|
||||
(val as OptionType)?.value
|
||||
);
|
||||
}}
|
||||
onChange={(val) =>
|
||||
handleDeliveryProductChange(idx, val)
|
||||
}
|
||||
options={getFilteredProductWarehouseOptions()}
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable
|
||||
@@ -1575,24 +1615,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
required
|
||||
placeholder='Pilih supplier...'
|
||||
value={delivery.supplier}
|
||||
onChange={(val) => {
|
||||
formik.setFieldTouched(
|
||||
`deliveries.${idx}.supplier`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`deliveries.${idx}.supplier`,
|
||||
val
|
||||
);
|
||||
formik.setFieldTouched(
|
||||
`deliveries.${idx}.supplier_id`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`deliveries.${idx}.supplier_id`,
|
||||
(val as OptionType)?.value
|
||||
);
|
||||
}}
|
||||
onChange={(val) =>
|
||||
handleDeliverySupplierChange(idx, val)
|
||||
}
|
||||
options={supplierOptions}
|
||||
onInputChange={setSupplierSelectInputValue}
|
||||
isLoading={isLoadingSuppliers}
|
||||
@@ -1684,20 +1709,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
<FileInput
|
||||
accept='.pdf,.jpg,.jpeg,.png'
|
||||
name={`deliveries.${idx}.document`}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
toast.error('Ukuran dokumen maksimal 5 MB!');
|
||||
e.target.value = '';
|
||||
return;
|
||||
onChange={(e) =>
|
||||
handleDeliveryDocumentChange(idx, e)
|
||||
}
|
||||
formik.setFieldValue(
|
||||
`deliveries.${idx}.document`,
|
||||
file
|
||||
);
|
||||
}
|
||||
}}
|
||||
{...isRepeaterInputError(
|
||||
'deliveries',
|
||||
'document',
|
||||
|
||||
@@ -91,7 +91,7 @@ const InventoryProductDetail = ({
|
||||
<td>:</td>
|
||||
<td>
|
||||
{inventoryProduct?.tax
|
||||
? formatCurrency(inventoryProduct?.tax)
|
||||
? formatNumber(inventoryProduct?.tax) + '%'
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
|
||||
import { TableToolbar } from '@/components/table/TableToolbar';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||
import {
|
||||
MarketingApi,
|
||||
SalesOrderApi,
|
||||
@@ -33,6 +33,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { useAuth } from '@/services/hooks/useAuth';
|
||||
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
const RowsOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
@@ -184,12 +185,16 @@ const MarketingTable = () => {
|
||||
const {
|
||||
options: productsOptions,
|
||||
isLoadingOptions: isLoadingProductsOptions,
|
||||
setInputValue: setProductsInputValue,
|
||||
loadMore: loadMoreProducts,
|
||||
} = useSelect(ProductApi.basePath, 'id', 'name', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
const {
|
||||
options: customersOptions,
|
||||
isLoadingOptions: isLoadingCustomersOptions,
|
||||
setInputValue: setCustomersInputValue,
|
||||
loadMore: loadMoreCustomers,
|
||||
} = useSelect(CustomerApi.basePath, 'id', 'name', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
@@ -400,6 +405,8 @@ const MarketingTable = () => {
|
||||
.join(',') || ''
|
||||
)
|
||||
}
|
||||
onInputChange={setProductsInputValue}
|
||||
onMenuScrollToBottom={loadMoreProducts}
|
||||
isMulti
|
||||
/>
|
||||
{/* select status */}
|
||||
@@ -444,6 +451,8 @@ const MarketingTable = () => {
|
||||
(value as OptionType)?.value.toString() || ''
|
||||
)
|
||||
}
|
||||
onInputChange={setCustomersInputValue}
|
||||
onMenuScrollToBottom={loadMoreCustomers}
|
||||
/>
|
||||
</TableRowSizeSelector>
|
||||
</div>
|
||||
@@ -512,8 +521,53 @@ const MarketingTable = () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'latest_approval.step_name',
|
||||
accessorKey: 'approval.step_name',
|
||||
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',
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
formatCurrency,
|
||||
formatDate,
|
||||
formatNumber,
|
||||
formatTitleCase,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import {
|
||||
@@ -34,6 +35,7 @@ import toast from 'react-hot-toast';
|
||||
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
||||
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import Badge from '@/components/Badge';
|
||||
|
||||
const MarketingDetail = ({
|
||||
initialValues,
|
||||
@@ -121,6 +123,10 @@ const MarketingDetail = ({
|
||||
);
|
||||
};
|
||||
|
||||
const approval = initialValues?.latest_approval;
|
||||
const isRejected = approval?.action == 'REJECTED';
|
||||
const isApproved = approval?.action == 'APPROVED';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col w-full gap-4'>
|
||||
@@ -230,7 +236,46 @@ const MarketingDetail = ({
|
||||
<tr>
|
||||
<td className='font-semibold'>Status</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>
|
||||
<td className='font-semibold'>Tanggal Penjualan</td>
|
||||
|
||||
@@ -633,7 +633,9 @@ const MarketingForm = ({
|
||||
isClearable
|
||||
placeholder='Pilih Pelanggan'
|
||||
isDisabled={
|
||||
formType === 'add_deliver' || formType === 'edit_deliver'
|
||||
formType === 'add_deliver' ||
|
||||
formType === 'edit_deliver' ||
|
||||
formType === 'edit'
|
||||
}
|
||||
/>
|
||||
<DateInput
|
||||
|
||||
@@ -7,6 +7,7 @@ import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
||||
import { format } from 'path';
|
||||
import { date } from 'yup';
|
||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface DeliveryOrderExportProps {
|
||||
data?: Marketing;
|
||||
@@ -23,7 +24,7 @@ const DeliveryOrderExport = ({
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!salesData) {
|
||||
alert('No sales order data available');
|
||||
toast.error('No sales order data available');
|
||||
return;
|
||||
}
|
||||
setIsGeneratingPDF(true);
|
||||
@@ -40,8 +41,7 @@ const DeliveryOrderExport = ({
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
toast.error('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface SalesOrderExportProps {
|
||||
data?: Marketing;
|
||||
@@ -17,7 +18,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!salesData) {
|
||||
alert('No sales order data available');
|
||||
toast.error('No sales order data available');
|
||||
return;
|
||||
}
|
||||
setIsGeneratingPDF(true);
|
||||
@@ -32,8 +33,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
toast.error('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import { AreaApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -164,7 +164,14 @@ const AreasTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { Bank } from '@/types/api/master-data/bank';
|
||||
import { BankApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -177,7 +177,14 @@ const BanksTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
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 { CustomerApi } from '@/services/api/master-data';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
@@ -186,7 +186,16 @@ const CustomersTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { Fcr } from '@/types/api/master-data/fcr';
|
||||
import { FcrApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -164,7 +164,14 @@ const FcrsTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -19,7 +19,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import Table from '@/components/Table';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
|
||||
const RowsOptions = ({
|
||||
@@ -33,22 +33,6 @@ const RowsOptions = ({
|
||||
}) => {
|
||||
return (
|
||||
<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'>
|
||||
<Button
|
||||
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
|
||||
@@ -65,6 +49,22 @@ const RowsOptions = ({
|
||||
Detail
|
||||
</Button>
|
||||
</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'>
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
@@ -182,7 +182,14 @@ const FlockTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -19,6 +19,8 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import Alert from '@/components/Alert';
|
||||
|
||||
interface FlockCustomProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -37,7 +39,13 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
setIsDeleteLoading(false);
|
||||
@@ -68,12 +76,29 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
||||
|
||||
// cek type form yang disubmit
|
||||
switch (formType) {
|
||||
case 'add':
|
||||
await FlockApi.create(payload);
|
||||
case 'add': {
|
||||
const createFlockRes = await FlockApi.create(payload);
|
||||
if (createFlockRes?.status === 'error') {
|
||||
setFlockFormErrorMessage(createFlockRes.message);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(createFlockRes?.message as string);
|
||||
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;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -174,6 +199,24 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
||||
)}
|
||||
|
||||
<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' && (
|
||||
<div
|
||||
@@ -197,17 +240,6 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
||||
</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>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { KandangApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -199,7 +199,16 @@ const KandangsTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -215,7 +215,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
required
|
||||
label='Nama'
|
||||
name='name'
|
||||
placeholder='Masukkan nama lokasi'
|
||||
placeholder='Masukkan nama kandang'
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
|
||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -186,7 +186,16 @@ const LocationsTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||
import { NonstockApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -198,7 +198,16 @@ const NonstocksTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -83,7 +83,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
||||
const formikInitialValues = useMemo<NonstockFormValues>(() => {
|
||||
return {
|
||||
name: initialValues?.name ?? '',
|
||||
uomId: initialValues?.uom_id ?? 0,
|
||||
uomId: initialValues?.uom?.id ?? 0,
|
||||
uom: initialValues?.uom
|
||||
? {
|
||||
value: initialValues?.uom?.id,
|
||||
@@ -229,7 +229,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
||||
required
|
||||
label='Nama'
|
||||
name='name'
|
||||
placeholder='Masukkan nama lokasi'
|
||||
placeholder='Masukkan nama nonstock'
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
|
||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -170,7 +170,16 @@ const ProductCategoryTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { ProductApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -230,8 +230,19 @@ const ProductsTable = () => {
|
||||
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully delete Product!');
|
||||
setIsDeleteLoading(false);
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as Yup from 'yup';
|
||||
type ProductFormSchemaType = {
|
||||
name: string;
|
||||
brand: string;
|
||||
sku: string;
|
||||
sku?: string;
|
||||
uom?: {
|
||||
value: number;
|
||||
label: string;
|
||||
@@ -15,10 +15,16 @@ type ProductFormSchemaType = {
|
||||
} | null;
|
||||
product_category_id: number;
|
||||
product_price: number | string;
|
||||
selling_price: number | string;
|
||||
tax: number | string;
|
||||
expiry_period: number | string;
|
||||
supplier_ids: number[];
|
||||
selling_price?: number | string;
|
||||
tax?: number | string;
|
||||
expiry_period?: number | string;
|
||||
suppliers: {
|
||||
supplier: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
price: number;
|
||||
}[];
|
||||
flags: string[];
|
||||
};
|
||||
|
||||
@@ -26,7 +32,7 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
|
||||
Yup.object({
|
||||
name: Yup.string().required('Nama wajib diisi!'),
|
||||
brand: Yup.string().required('Merek wajib diisi!'),
|
||||
sku: Yup.string().required('SKU wajib diisi!'),
|
||||
sku: Yup.string(),
|
||||
|
||||
uom: Yup.object({
|
||||
value: Yup.number()
|
||||
@@ -58,24 +64,34 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
|
||||
.min(1, 'Harga produk tidak boleh kurang dari 1!'),
|
||||
|
||||
selling_price: Yup.number()
|
||||
.required('Harga jual wajib diisi!')
|
||||
.typeError('Harga jual wajib diisi!')
|
||||
.typeError('Harga hanya boleh angka!')
|
||||
.min(1, 'Harga jual tidak boleh kurang dari 1!'),
|
||||
|
||||
tax: Yup.number()
|
||||
.required('Pajak wajib diisi!')
|
||||
.typeError('Pajak wajib diisi!')
|
||||
.typeError('Pajak hanya boleh angka!')
|
||||
.min(0, 'Pajak tidak boleh kurang dari 0!')
|
||||
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
|
||||
|
||||
expiry_period: Yup.number()
|
||||
.required('Periode kadaluarsa wajib diisi!')
|
||||
.typeError('Periode kadaluarsa wajib diisi!')
|
||||
.typeError('Periode kadaluarsa hanya boleh angka!')
|
||||
.min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
|
||||
|
||||
supplier_ids: Yup.array()
|
||||
.of(Yup.number().required().typeError('Supplier tidak valid!'))
|
||||
.min(1, 'Minimal harus ada 1 supplier!')
|
||||
suppliers: Yup.array()
|
||||
.of(
|
||||
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!'),
|
||||
|
||||
flags: Yup.array()
|
||||
|
||||
@@ -41,6 +41,8 @@ import { cn } from '@/lib/helper';
|
||||
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import Card from '@/components/Card';
|
||||
import { removeArrayItemAndSync } from '@/lib/utils/formik';
|
||||
|
||||
interface ProductFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -101,7 +103,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
selling_price: initialValues?.selling_price ?? '',
|
||||
tax: initialValues?.tax ?? '',
|
||||
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 ?? [],
|
||||
}),
|
||||
[initialValues]
|
||||
@@ -120,12 +130,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
uom_id: values.uom_id,
|
||||
product_category_id: values.product_category_id,
|
||||
product_price: parseInt(values.product_price.toString()) || 0,
|
||||
selling_price: parseInt(values.selling_price.toString()) || 0,
|
||||
tax: parseInt(values.tax.toString()) || 0,
|
||||
expiry_period: parseInt(values.expiry_period.toString()) || 0,
|
||||
supplier_ids: values.supplier_ids.filter(
|
||||
(id): id is number => typeof id === 'number'
|
||||
),
|
||||
selling_price: values.selling_price
|
||||
? parseInt(values.selling_price.toString()) || 0
|
||||
: undefined,
|
||||
tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
|
||||
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'),
|
||||
};
|
||||
switch (type) {
|
||||
@@ -179,13 +194,29 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
category: 'SAPRONAK',
|
||||
});
|
||||
|
||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||
formik.setFieldTouched('supplier_ids', true);
|
||||
formik.setFieldValue(
|
||||
'supplier_ids',
|
||||
arr.map((v) => (v as OptionType).value)
|
||||
const filteredSupplierOptions = useMemo(() => {
|
||||
return supplierOptions.filter((opt) => {
|
||||
return !formik.values.suppliers.some(
|
||||
(s) => s.supplier?.value === opt.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 = () => {
|
||||
@@ -201,6 +232,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
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(() => {
|
||||
formikSetValues(formikInitialValues);
|
||||
}, [formikSetValues, formikInitialValues]);
|
||||
@@ -271,7 +315,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label='SKU'
|
||||
name='sku'
|
||||
placeholder='Masukkan SKU...'
|
||||
@@ -344,7 +387,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
<NumberInput
|
||||
required
|
||||
label='Harga Jual'
|
||||
name='selling_price'
|
||||
placeholder='Masukkan harga jual...'
|
||||
@@ -366,7 +408,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
</div>
|
||||
<div className='grid sm:grid-cols-2 gap-4'>
|
||||
<NumberInput
|
||||
required
|
||||
label='Pajak (%)'
|
||||
name='tax'
|
||||
placeholder='Masukkan pajak...'
|
||||
@@ -383,7 +424,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
<NumberInput
|
||||
required
|
||||
label='Periode Kadaluarsa (hari)'
|
||||
name='expiry_period'
|
||||
placeholder='Masukkan periode kadaluarsa...'
|
||||
@@ -403,28 +443,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid sm:grid-cols-2 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
|
||||
/>
|
||||
<div className='grid sm:grid-cols-1 gap-4'>
|
||||
<SelectInput
|
||||
required
|
||||
label='Flags'
|
||||
@@ -447,6 +466,129 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
isClearable
|
||||
/>
|
||||
</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 className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'add' && (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ProductionStandard } from '@/types/api/master-data/production-standard'
|
||||
import { Icon } from '@iconify/react';
|
||||
import useSWR from 'swr';
|
||||
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 { CellContext } from '@tanstack/react-table';
|
||||
import { useModal } from '@/components/Modal';
|
||||
@@ -94,9 +94,16 @@ const ProductionStandardTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await ProductionStandardApi.delete(
|
||||
const deleteResponse = await ProductionStandardApi.delete(
|
||||
selectedProductionStandard?.id as number
|
||||
);
|
||||
|
||||
if (isResponseError(deleteResponse)) {
|
||||
toast.error(deleteResponse.message);
|
||||
setIsDeleteLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
refreshProductionStandards();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
+15
-19
@@ -2,34 +2,30 @@ import * as Yup from 'yup';
|
||||
|
||||
// Schema for LAYING category (production_standard_details is required)
|
||||
const LayingRepeaterFormSchema = Yup.object({
|
||||
week: Yup.number().required('Minggu wajib diisi!'),
|
||||
week: Yup.number().required('Wajib diisi!'),
|
||||
production_standard_uniformity_details: Yup.object({
|
||||
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
||||
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
||||
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
||||
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
||||
target_mean_bw: Yup.number().required('Wajib diisi!'),
|
||||
max_depletion: Yup.number().required('Wajib diisi!'),
|
||||
min_uniformity: Yup.number().required('Wajib diisi!'),
|
||||
feed_intake: Yup.number().required('Wajib diisi!'),
|
||||
}),
|
||||
production_standard_details: Yup.object({
|
||||
target_hen_day_production: Yup.number().required(
|
||||
'Produksi telur per hari wajib diisi!'
|
||||
),
|
||||
target_hen_house_production: Yup.number().required(
|
||||
'Produksi telur per kandang 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!'),
|
||||
target_hen_day_production: Yup.number().required('Wajib diisi!'),
|
||||
target_hen_house_production: Yup.number().required('Wajib diisi!'),
|
||||
target_egg_weight: Yup.number().required('Wajib diisi!'),
|
||||
target_egg_mass: Yup.number().required('Wajib diisi!'),
|
||||
standard_fcr: Yup.number().required('Wajib diisi!'),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
// Schema for GROWING category (production_standard_details is optional)
|
||||
const GrowingRepeaterFormSchema = Yup.object({
|
||||
week: Yup.number().required('Minggu wajib diisi!'),
|
||||
week: Yup.number().required('Wajib diisi!'),
|
||||
production_standard_uniformity_details: Yup.object({
|
||||
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
||||
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
||||
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
||||
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
||||
target_mean_bw: Yup.number().required('Wajib diisi!'),
|
||||
max_depletion: Yup.number().required('Wajib diisi!'),
|
||||
min_uniformity: Yup.number().required('Wajib diisi!'),
|
||||
feed_intake: Yup.number().required('Wajib diisi!'),
|
||||
}),
|
||||
production_standard_details: Yup.object({
|
||||
target_hen_day_production: Yup.number().optional(),
|
||||
|
||||
+85
-44
@@ -344,7 +344,7 @@ const ProductionStandardForm = ({
|
||||
const columns = useMemo<ColumnDef<TableRowsType>[]>(() => {
|
||||
const baseColumns: ColumnDef<TableRowsType>[] = [
|
||||
{
|
||||
header: 'Minggu',
|
||||
header: 'Week',
|
||||
accessorKey: 'week',
|
||||
enableSorting: false,
|
||||
},
|
||||
@@ -358,30 +358,40 @@ const ProductionStandardForm = ({
|
||||
header: 'Hen Day',
|
||||
accessorFn: (row) =>
|
||||
row.production_standard_details?.target_hen_day_production,
|
||||
cell: ({ row }) =>
|
||||
`${row.original.production_standard_details?.target_hen_day_production}%`,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
header: 'Hen House',
|
||||
accessorFn: (row) =>
|
||||
row.production_standard_details?.target_hen_house_production,
|
||||
cell: ({ row }) =>
|
||||
`${row.original.production_standard_details?.target_hen_house_production} pc`,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
header: 'Egg Weight',
|
||||
accessorFn: (row) =>
|
||||
row.production_standard_details?.target_egg_weight,
|
||||
cell: ({ row }) =>
|
||||
`${row.original.production_standard_details?.target_egg_weight} g`,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
header: 'Egg Mass',
|
||||
accessorFn: (row) =>
|
||||
row.production_standard_details?.target_egg_mass,
|
||||
cell: ({ row }) =>
|
||||
`${row.original.production_standard_details?.target_egg_mass} g`,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
header: 'FCR',
|
||||
accessorFn: (row) =>
|
||||
row.production_standard_details?.standard_fcr,
|
||||
cell: ({ row }) =>
|
||||
`${row.original.production_standard_details?.standard_fcr} g`,
|
||||
enableSorting: false,
|
||||
},
|
||||
]
|
||||
@@ -393,24 +403,32 @@ const ProductionStandardForm = ({
|
||||
header: 'Mean BW',
|
||||
accessorFn: (row) =>
|
||||
row.production_standard_uniformity_details?.target_mean_bw,
|
||||
cell: ({ row }) =>
|
||||
`${row.original.production_standard_uniformity_details?.target_mean_bw} g`,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
header: 'Max Depletion',
|
||||
accessorFn: (row) =>
|
||||
row.production_standard_uniformity_details?.max_depletion,
|
||||
cell: ({ row }) =>
|
||||
`${row.original.production_standard_uniformity_details?.max_depletion}%`,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
header: 'Min Uniformity',
|
||||
accessorFn: (row) =>
|
||||
row.production_standard_uniformity_details?.min_uniformity,
|
||||
cell: ({ row }) =>
|
||||
`${row.original.production_standard_uniformity_details?.min_uniformity}%`,
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
header: 'Feed Intake',
|
||||
accessorFn: (row) =>
|
||||
row.production_standard_uniformity_details?.feed_intake,
|
||||
cell: ({ row }) =>
|
||||
`${row.original.production_standard_uniformity_details?.feed_intake} g`,
|
||||
enableSorting: false,
|
||||
},
|
||||
];
|
||||
@@ -728,7 +746,52 @@ const ProductionStandardForm = ({
|
||||
};
|
||||
|
||||
// ===== 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 (
|
||||
<>
|
||||
@@ -821,19 +884,20 @@ const ProductionStandardForm = ({
|
||||
key={`row-${row.index}`}
|
||||
className='sticky bottom-0 bg-base-100 shadow-lg'
|
||||
>
|
||||
<td colSpan={colSpan} className='p-6'>
|
||||
<td colSpan={colSpan} className='p-2'>
|
||||
<form
|
||||
className='h-full w-full flex flex-col justify-end'
|
||||
onSubmit={repeaterFormik.handleSubmit}
|
||||
onReset={repeaterFormik.handleReset}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-4 items-start',
|
||||
className='grid gap-2 items-start w-full'
|
||||
style={{
|
||||
gridTemplateColumns:
|
||||
formik.values.project_category === 'LAYING'
|
||||
? 'grid-cols-10'
|
||||
: 'grid-cols-5'
|
||||
)}
|
||||
? 'repeat(10, minmax(auto, 1fr)) minmax(auto, auto)'
|
||||
: 'repeat(4, minmax(auto, 1fr)) minmax(auto, auto)',
|
||||
}}
|
||||
>
|
||||
<NumberInput
|
||||
name='week'
|
||||
@@ -862,7 +926,7 @@ const ProductionStandardForm = ({
|
||||
}
|
||||
onChange={repeaterFormik.handleChange}
|
||||
onBlur={repeaterFormik.handleBlur}
|
||||
endAdornment={<Icon icon='mdi:percent' />}
|
||||
bottomLabel='Persen (%)'
|
||||
errorMessage={getProductionDetailsError(
|
||||
repeaterFormik.errors
|
||||
.production_standard_details,
|
||||
@@ -894,11 +958,7 @@ const ProductionStandardForm = ({
|
||||
}
|
||||
onChange={repeaterFormik.handleChange}
|
||||
onBlur={repeaterFormik.handleBlur}
|
||||
endAdornment={
|
||||
<div className='w-full h-full flex items-center justify-center'>
|
||||
Butir
|
||||
</div>
|
||||
}
|
||||
bottomLabel='Butir (pc)'
|
||||
errorMessage={getProductionDetailsError(
|
||||
repeaterFormik.errors
|
||||
.production_standard_details,
|
||||
@@ -930,11 +990,7 @@ const ProductionStandardForm = ({
|
||||
}
|
||||
onChange={repeaterFormik.handleChange}
|
||||
onBlur={repeaterFormik.handleBlur}
|
||||
endAdornment={
|
||||
<div className='w-full h-full flex items-center justify-center'>
|
||||
gr
|
||||
</div>
|
||||
}
|
||||
bottomLabel='Gram (g)'
|
||||
errorMessage={getProductionDetailsError(
|
||||
repeaterFormik.errors
|
||||
.production_standard_details,
|
||||
@@ -959,17 +1015,13 @@ const ProductionStandardForm = ({
|
||||
name='production_standard_details.target_egg_mass'
|
||||
label='Egg Mass'
|
||||
placeholder='1'
|
||||
bottomLabel='Gram (g)'
|
||||
value={
|
||||
repeaterFormik.values
|
||||
.production_standard_details?.target_egg_mass
|
||||
}
|
||||
onChange={repeaterFormik.handleChange}
|
||||
onBlur={repeaterFormik.handleBlur}
|
||||
endAdornment={
|
||||
<div className='w-full h-full flex items-center justify-center'>
|
||||
gr
|
||||
</div>
|
||||
}
|
||||
errorMessage={getProductionDetailsError(
|
||||
repeaterFormik.errors
|
||||
.production_standard_details,
|
||||
@@ -1000,11 +1052,7 @@ const ProductionStandardForm = ({
|
||||
}
|
||||
onChange={repeaterFormik.handleChange}
|
||||
onBlur={repeaterFormik.handleBlur}
|
||||
endAdornment={
|
||||
<div className='w-full h-full flex items-center justify-center'>
|
||||
gr
|
||||
</div>
|
||||
}
|
||||
bottomLabel='Gram (g)'
|
||||
errorMessage={getProductionDetailsError(
|
||||
repeaterFormik.errors
|
||||
.production_standard_details,
|
||||
@@ -1038,11 +1086,7 @@ const ProductionStandardForm = ({
|
||||
}
|
||||
onChange={repeaterFormik.handleChange}
|
||||
onBlur={repeaterFormik.handleBlur}
|
||||
endAdornment={
|
||||
<div className='w-full h-full flex items-center justify-center'>
|
||||
gr
|
||||
</div>
|
||||
}
|
||||
bottomLabel='Gram (g)'
|
||||
errorMessage={
|
||||
repeaterFormik.errors
|
||||
.production_standard_uniformity_details
|
||||
@@ -1072,7 +1116,7 @@ const ProductionStandardForm = ({
|
||||
}
|
||||
onChange={repeaterFormik.handleChange}
|
||||
onBlur={repeaterFormik.handleBlur}
|
||||
endAdornment={<Icon icon='mdi:percent' />}
|
||||
bottomLabel='Persen (%)'
|
||||
errorMessage={
|
||||
repeaterFormik.errors
|
||||
.production_standard_uniformity_details
|
||||
@@ -1102,7 +1146,7 @@ const ProductionStandardForm = ({
|
||||
}
|
||||
onChange={repeaterFormik.handleChange}
|
||||
onBlur={repeaterFormik.handleBlur}
|
||||
endAdornment={<Icon icon='mdi:percent' />}
|
||||
bottomLabel='Persen (%)'
|
||||
errorMessage={
|
||||
repeaterFormik.errors
|
||||
.production_standard_uniformity_details
|
||||
@@ -1132,11 +1176,8 @@ const ProductionStandardForm = ({
|
||||
}
|
||||
onChange={repeaterFormik.handleChange}
|
||||
onBlur={repeaterFormik.handleBlur}
|
||||
endAdornment={
|
||||
<div className='w-full h-full flex items-center justify-center'>
|
||||
gr/ekor
|
||||
</div>
|
||||
}
|
||||
bottomLabel='Gram/Ekor (g)'
|
||||
endAdornment
|
||||
errorMessage={
|
||||
repeaterFormik.errors
|
||||
.production_standard_uniformity_details
|
||||
@@ -1162,7 +1203,7 @@ const ProductionStandardForm = ({
|
||||
type='button'
|
||||
color='error'
|
||||
variant='outline'
|
||||
className='min-w-24'
|
||||
className='min-w-xs'
|
||||
onClick={handleCancelEdit}
|
||||
>
|
||||
<Icon icon='mdi:close' /> Batal
|
||||
@@ -1178,7 +1219,7 @@ const ProductionStandardForm = ({
|
||||
<Button
|
||||
type='submit'
|
||||
color={editMode ? 'warning' : 'success'}
|
||||
className='min-w-24'
|
||||
className='min-w-xs'
|
||||
disabled={
|
||||
isAddingRow ||
|
||||
formik.values.project_category === ''
|
||||
@@ -1195,7 +1236,7 @@ const ProductionStandardForm = ({
|
||||
variant='outline'
|
||||
color='primary'
|
||||
onClick={toggleTableHeight}
|
||||
className='absolute bottom-6 right-6'
|
||||
className='absolute bottom-2 right-2'
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
|
||||
@@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
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 { SupplierApi } from '@/services/api/master-data';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
@@ -205,7 +205,16 @@ const SuppliersTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { Uom } from '@/types/api/master-data/uom';
|
||||
import { UomApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -164,7 +164,14 @@ const UomsTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { WarehouseApi } from '@/services/api/master-data';
|
||||
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 { ROWS_OPTIONS } from '@/config/constant';
|
||||
|
||||
@@ -220,7 +220,16 @@ const WarehousesTable = () => {
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
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();
|
||||
|
||||
deleteModal.closeModal();
|
||||
|
||||
@@ -330,7 +330,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
||||
required
|
||||
label='Nama'
|
||||
name='name'
|
||||
placeholder='Masukkan nama lokasi'
|
||||
placeholder='Masukkan nama warehouse'
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
|
||||
@@ -75,12 +75,12 @@ const ChickinFormKandang = ({
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Badge
|
||||
variant='soft'
|
||||
color='success'
|
||||
color='primary'
|
||||
className={{
|
||||
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
|
||||
</Badge>
|
||||
<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 CheckboxInput from '@/components/input/CheckboxInput';
|
||||
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 ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import Table from '@/components/Table';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
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 { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
@@ -59,9 +62,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
const selectedRowIds = Object.keys(rowSelection)
|
||||
.filter((id) => rowSelection[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 [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
@@ -90,55 +90,25 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
{ revalidateOnMount: true }
|
||||
);
|
||||
|
||||
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
|
||||
search: areaSelectInputValue,
|
||||
limit: '100',
|
||||
}).toString()}`;
|
||||
const { data: areas, isLoading: isLoadingAreas } = useSWR(
|
||||
areaUrl,
|
||||
AreaApi.getAllFetcher
|
||||
);
|
||||
|
||||
const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({
|
||||
search: locationSelectInputValue,
|
||||
area_id: selectedArea != null ? selectedArea.value.toString() : '',
|
||||
limit: '100',
|
||||
}).toString()}`;
|
||||
const { data: locations, isLoading: isLoadingLocations } = useSWR(
|
||||
locationUrl,
|
||||
LocationApi.getAllFetcher
|
||||
);
|
||||
|
||||
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,
|
||||
}))
|
||||
: [];
|
||||
// ===== Fetch Data Select =====
|
||||
const {
|
||||
options: optionsArea,
|
||||
isLoadingOptions: isLoadingArea,
|
||||
setInputValue: setAreaSelectInputValue,
|
||||
loadMore: loadMoreArea,
|
||||
} = useSelect(AreaApi.basePath, 'id', 'name');
|
||||
const {
|
||||
options: optionsLocation,
|
||||
isLoadingOptions: isLoadingLocation,
|
||||
setInputValue: setLocationSelectInputValue,
|
||||
loadMore: loadMoreLocation,
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name');
|
||||
const {
|
||||
options: optionsKandang,
|
||||
isLoadingOptions: isLoadingKandang,
|
||||
setInputValue: setKandangSelectInputValue,
|
||||
loadMore: loadMoreKandang,
|
||||
} = useSelect(KandangApi.basePath, 'id', 'name');
|
||||
|
||||
// ====== HANDLER ======
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
@@ -284,7 +254,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
header: 'Status',
|
||||
cell: (props) => {
|
||||
const approval = props.row.original.approval;
|
||||
|
||||
const isRejected = approval?.action == 'REJECTED';
|
||||
const isApproved = approval?.action == 'APPROVED';
|
||||
return (
|
||||
<Badge
|
||||
variant='soft'
|
||||
@@ -292,11 +263,17 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
badge: 'rounded-lg px-2 w-full flex flex-row justify-start',
|
||||
}}
|
||||
color={
|
||||
approval?.step_number == 1
|
||||
isRejected
|
||||
? 'error'
|
||||
: isApproved
|
||||
? approval?.step_number == 1
|
||||
? 'neutral'
|
||||
: approval?.step_number == 2
|
||||
? 'primary'
|
||||
: approval?.step_number == 3
|
||||
? 'success'
|
||||
: 'error'
|
||||
: 'neutral'
|
||||
: 'neutral'
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
@@ -307,11 +284,15 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
approval?.step_number == 1
|
||||
? 'neutral'
|
||||
: approval?.step_number == 2
|
||||
? 'primary'
|
||||
: approval?.step_number == 3
|
||||
? 'success'
|
||||
: 'error'
|
||||
: 'neutral'
|
||||
}
|
||||
/>
|
||||
{approval?.step_name}
|
||||
{isRejected
|
||||
? 'Ditolak'
|
||||
: formatTitleCase(approval?.step_name || '')}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
@@ -385,7 +366,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
<SelectInput
|
||||
label='Area'
|
||||
options={optionsArea}
|
||||
isLoading={isLoadingAreas}
|
||||
isLoading={isLoadingArea}
|
||||
value={selectedArea}
|
||||
onChange={(val) => {
|
||||
setSelectedArea(val as OptionType);
|
||||
@@ -395,12 +376,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
);
|
||||
}}
|
||||
onInputChange={setAreaSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreArea}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
options={optionsLocation}
|
||||
isLoading={isLoadingLocations}
|
||||
isLoading={isLoadingLocation}
|
||||
value={selectedLocation}
|
||||
onChange={(val) => {
|
||||
setSelectedLocation(val as OptionType);
|
||||
@@ -410,6 +392,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
);
|
||||
}}
|
||||
onInputChange={setLocationSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocation}
|
||||
isClearable
|
||||
/>
|
||||
<SelectInput
|
||||
@@ -425,6 +408,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
);
|
||||
}}
|
||||
onInputChange={setKandangSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreKandang}
|
||||
isClearable
|
||||
/>
|
||||
<DebouncedTextInput
|
||||
|
||||
@@ -156,9 +156,9 @@ const ProjectFlockDetail = ({
|
||||
projectFlock.approval?.step_number == 1
|
||||
? 'neutral'
|
||||
: projectFlock.approval?.step_number == 2
|
||||
? 'primary'
|
||||
: projectFlock.approval?.step_number == 3
|
||||
? 'success'
|
||||
: projectFlock.approval?.step_number >= 3
|
||||
? 'error'
|
||||
: undefined
|
||||
}
|
||||
className={{
|
||||
@@ -173,9 +173,9 @@ const ProjectFlockDetail = ({
|
||||
projectFlock.approval?.step_number == 1
|
||||
? 'neutral'
|
||||
: projectFlock.approval?.step_number == 2
|
||||
? 'primary'
|
||||
: projectFlock.approval?.step_number == 3
|
||||
? 'success'
|
||||
: projectFlock.approval?.step_number >= 3
|
||||
? 'error'
|
||||
: undefined
|
||||
}
|
||||
/>{' '}
|
||||
@@ -273,7 +273,7 @@ const ProjectFlockDetail = ({
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Badge
|
||||
variant='soft'
|
||||
color={'success'}
|
||||
color={'primary'}
|
||||
className={{
|
||||
badge: 'rounded-lg px-2',
|
||||
}}
|
||||
@@ -282,7 +282,7 @@ const ProjectFlockDetail = ({
|
||||
icon='mdi:circle'
|
||||
width={12}
|
||||
height={12}
|
||||
color={'success'}
|
||||
color={'primary'}
|
||||
/>{' '}
|
||||
Kandang Aktif ({projectFlock.kandangs?.length})
|
||||
</Badge>
|
||||
|
||||
@@ -102,41 +102,54 @@ const ProjectFlockForm = ({
|
||||
);
|
||||
|
||||
// Fetch Data
|
||||
const { isLoadingOptions: isLoadingFlocks, options: optionsFlock } =
|
||||
useSelect(FlockApi.basePath, 'id', 'name');
|
||||
const {
|
||||
setInputValue: setInputValueFlock,
|
||||
isLoadingOptions: isLoadingFlocks,
|
||||
options: optionsFlock,
|
||||
loadMore: loadMoreFlock,
|
||||
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
||||
project_category: selectedCategory,
|
||||
});
|
||||
|
||||
const { options: optionsArea, isLoadingOptions: isLoadingAreas } = useSelect(
|
||||
AreaApi.basePath,
|
||||
'id',
|
||||
'name'
|
||||
);
|
||||
const {
|
||||
setInputValue: setInputValueArea,
|
||||
options: optionsArea,
|
||||
isLoadingOptions: isLoadingAreas,
|
||||
loadMore: loadMoreArea,
|
||||
} = useSelect(AreaApi.basePath, 'id', 'name');
|
||||
|
||||
const { options: optionsLocation, isLoadingOptions: isLoadingLocations } =
|
||||
useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||
const {
|
||||
options: optionsLocation,
|
||||
isLoadingOptions: isLoadingLocations,
|
||||
setInputValue: setInputValueLocation,
|
||||
loadMore: loadMoreLocation,
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||
area_id:
|
||||
selectedArea != ''
|
||||
? selectedArea
|
||||
: ((initialValues?.area?.id ?? '') as string),
|
||||
});
|
||||
|
||||
const { options: optionsFcr, isLoadingOptions: isLoadingFcrs } = useSelect(
|
||||
FcrApi.basePath,
|
||||
'id',
|
||||
'name'
|
||||
);
|
||||
const {
|
||||
options: optionsFcr,
|
||||
isLoadingOptions: isLoadingFcrs,
|
||||
setInputValue: setInputValueFcr,
|
||||
loadMore: loadMoreFcr,
|
||||
} = useSelect(FcrApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
options: optionsProductionStandards,
|
||||
isLoadingOptions: isLoadingProductionStandards,
|
||||
setInputValue: setInputValueProductionStandard,
|
||||
loadMore: loadMoreProductionStandard,
|
||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
||||
search: '',
|
||||
project_category: selectedCategory,
|
||||
});
|
||||
|
||||
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
|
||||
search: '',
|
||||
location_id: selectedLocation == '' ? '0' : selectedLocation,
|
||||
limit: 'limit',
|
||||
limit: '500',
|
||||
}).toString()}`;
|
||||
const {
|
||||
data: kandang,
|
||||
@@ -153,6 +166,8 @@ const ProjectFlockForm = ({
|
||||
options: optionsNonstock,
|
||||
rawData: nonstocks,
|
||||
isLoadingOptions: isLoadingNonstocks,
|
||||
setInputValue: setInputValueNonstock,
|
||||
loadMore: loadMoreNonstock,
|
||||
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -542,15 +557,12 @@ const ProjectFlockForm = ({
|
||||
};
|
||||
|
||||
const onDeleteBudgetRowHandler = (nonstock_id: number, index?: number) => {
|
||||
console.log(`nonstock_id: ${nonstock_id}, index: ${index}`);
|
||||
if (!nonstock_id) {
|
||||
const updatedBudgets = formik.values.project_budgets
|
||||
.map((budget, i) => {
|
||||
if (i == index) {
|
||||
console.log(`buget: ${null}, index: ${index}, i: ${i}`);
|
||||
return null;
|
||||
} else {
|
||||
console.log(`buget: ${budget}, index: ${index}, i: ${i}`);
|
||||
return budget;
|
||||
}
|
||||
})
|
||||
@@ -722,6 +734,8 @@ const ProjectFlockForm = ({
|
||||
formik.touched.area_id && Boolean(formik.errors.area_id)
|
||||
}
|
||||
errorMessage={formik.errors.area_id as string}
|
||||
onInputChange={setInputValueArea}
|
||||
onMenuScrollToBottom={loadMoreArea}
|
||||
isClearable
|
||||
isDisabled={formType != 'add'}
|
||||
/>
|
||||
@@ -740,6 +754,8 @@ const ProjectFlockForm = ({
|
||||
formik.touched.location_id &&
|
||||
Boolean(formik.errors.location_id)
|
||||
}
|
||||
onInputChange={setInputValueLocation}
|
||||
onMenuScrollToBottom={loadMoreLocation}
|
||||
errorMessage={formik.errors.location_id as string}
|
||||
isClearable
|
||||
isDisabled={formType != 'add' || disabledLocation}
|
||||
@@ -766,6 +782,8 @@ const ProjectFlockForm = ({
|
||||
);
|
||||
}}
|
||||
options={optionsFlock}
|
||||
onInputChange={setInputValueFlock}
|
||||
onMenuScrollToBottom={loadMoreFlock}
|
||||
isLoading={isLoadingFlocks}
|
||||
isError={
|
||||
formik.touched.flock_name && Boolean(formik.errors.flock_name)
|
||||
@@ -781,6 +799,8 @@ const ProjectFlockForm = ({
|
||||
onChange={(val) => {
|
||||
optionChangeHandler(val, 'fcr');
|
||||
}}
|
||||
onInputChange={setInputValueFcr}
|
||||
onMenuScrollToBottom={loadMoreFcr}
|
||||
options={optionsFcr}
|
||||
isLoading={isLoadingFcrs}
|
||||
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
|
||||
@@ -808,6 +828,8 @@ const ProjectFlockForm = ({
|
||||
onChange={(val) => {
|
||||
optionChangeHandler(val, 'production_standard');
|
||||
}}
|
||||
onInputChange={setInputValueProductionStandard}
|
||||
onMenuScrollToBottom={loadMoreProductionStandard}
|
||||
options={optionsProductionStandards}
|
||||
isLoading={isLoadingProductionStandards}
|
||||
isError={
|
||||
@@ -892,6 +914,8 @@ const ProjectFlockForm = ({
|
||||
isLoading={isLoadingNonstocks}
|
||||
placeholder='Pilih barang non stock'
|
||||
value={formik.values.project_budgets[index].nonstock}
|
||||
onInputChange={setInputValueNonstock}
|
||||
onMenuScrollToBottom={loadMoreNonstock}
|
||||
onChange={(val) => {
|
||||
const updatedBudgets = [
|
||||
...formik.values.project_budgets,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RefObject } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
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 { useModal } from '@/components/Modal';
|
||||
import Modal from '@/components/Modal';
|
||||
@@ -656,30 +656,52 @@ const RecordingTable = () => {
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const recording = row.original;
|
||||
const isDisabled = isRecordingApproved(recording);
|
||||
|
||||
const handleToggleSelection = (e: unknown) => {
|
||||
if (!isDisabled) {
|
||||
row.getToggleSelectedHandler()(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={cn({ 'opacity-50': isDisabled })}>
|
||||
<CheckboxInput
|
||||
name='row'
|
||||
checked={row.getIsSelected()}
|
||||
indeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
onChange={handleToggleSelection}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: '#',
|
||||
header: 'No',
|
||||
cell: (props) =>
|
||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||
props.row.index +
|
||||
1,
|
||||
},
|
||||
{
|
||||
header: 'Nama Project',
|
||||
header: 'Lokasi',
|
||||
cell: (props) => props.row.original.location?.name || '-',
|
||||
},
|
||||
{
|
||||
header: 'Flock',
|
||||
cell: (props) =>
|
||||
props.row.original.project_flock?.flock_name || '-',
|
||||
},
|
||||
{
|
||||
header: 'Kandang',
|
||||
cell: (props) => props.row.original.kandang?.name || '-',
|
||||
},
|
||||
{
|
||||
header: 'Periode',
|
||||
cell: (props) => props.row.original.project_flock?.period || '-',
|
||||
},
|
||||
{
|
||||
header: 'Kategori',
|
||||
cell: (props) => {
|
||||
@@ -696,19 +718,280 @@ const RecordingTable = () => {
|
||||
},
|
||||
{
|
||||
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',
|
||||
cell: (props) =>
|
||||
formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'),
|
||||
},
|
||||
{
|
||||
header: 'Populasi Awal',
|
||||
header: 'Populasi Akhir',
|
||||
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',
|
||||
@@ -730,21 +1013,6 @@ const RecordingTable = () => {
|
||||
approvalHistoryModal.openModal();
|
||||
};
|
||||
|
||||
const getStatusText = (action: string) => {
|
||||
switch (action) {
|
||||
case 'APPROVED':
|
||||
return 'Disetujui';
|
||||
case 'REJECTED':
|
||||
return 'Ditolak';
|
||||
case 'CREATED':
|
||||
return 'Dibuat';
|
||||
case 'UPDATED':
|
||||
return 'Diperbarui';
|
||||
default:
|
||||
return action;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant='soft'
|
||||
@@ -755,7 +1023,7 @@ const RecordingTable = () => {
|
||||
}}
|
||||
onClick={openApprovalHistory}
|
||||
>
|
||||
{getStatusText(approval.action)}
|
||||
{approval.step_name || approval.action}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
@@ -874,14 +1142,15 @@ const RecordingTable = () => {
|
||||
'mb-20':
|
||||
isResponseSuccess(recordings) && recordings?.data?.length === 0,
|
||||
}),
|
||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200',
|
||||
headerColumnClassName:
|
||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
'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:
|
||||
'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:
|
||||
'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';
|
||||
|
||||
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: {
|
||||
value: number;
|
||||
label: string;
|
||||
@@ -17,16 +33,16 @@ type RecordingGrowingFormSchemaType = {
|
||||
qty: number | string;
|
||||
}[];
|
||||
depletions: {
|
||||
product_warehouse_id: number;
|
||||
qty: number | string;
|
||||
product_warehouse_id?: number;
|
||||
qty?: number | string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
||||
eggs: {
|
||||
product_warehouse_id: number;
|
||||
qty: number | string;
|
||||
weight: number | string;
|
||||
product_warehouse_id?: number;
|
||||
qty?: number | string;
|
||||
weight?: number | string;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -36,14 +52,14 @@ export type StockSchema = {
|
||||
};
|
||||
|
||||
export type DepletionSchema = {
|
||||
product_warehouse_id: number;
|
||||
qty: number | string;
|
||||
product_warehouse_id?: number;
|
||||
qty?: number | string;
|
||||
};
|
||||
|
||||
export type EggSchema = {
|
||||
product_warehouse_id: number;
|
||||
qty: number | string;
|
||||
weight: number | string;
|
||||
product_warehouse_id?: number;
|
||||
qty?: number | string;
|
||||
weight?: number | string;
|
||||
};
|
||||
|
||||
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
||||
@@ -59,32 +75,51 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
||||
|
||||
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
||||
product_warehouse_id: Yup.number()
|
||||
.required('Produk depletions wajib diisi!')
|
||||
.min(1, 'Produk depletions wajib diisi!')
|
||||
.typeError('Produk depletions harus berupa angka!'),
|
||||
.optional()
|
||||
.typeError('Depletions harus berupa angka!'),
|
||||
qty: Yup.number()
|
||||
.required('Jumlah depletions wajib diisi!')
|
||||
.min(1, 'Jumlah depletions minimal 1!')
|
||||
.optional()
|
||||
.typeError('Jumlah depletions harus berupa angka!'),
|
||||
});
|
||||
|
||||
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
||||
product_warehouse_id: Yup.number()
|
||||
.required('Kondisi telur wajib diisi!')
|
||||
.min(1, 'Kondisi telur wajib diisi!')
|
||||
.optional()
|
||||
.typeError('Kondisi telur harus berupa angka!'),
|
||||
qty: Yup.number()
|
||||
.required('Jumlah telur wajib diisi!')
|
||||
.min(1, 'Jumlah telur tidak boleh 0!')
|
||||
.typeError('Jumlah telur harus berupa angka!'),
|
||||
weight: Yup.number()
|
||||
.required('Berat telur wajib diisi!')
|
||||
.min(1, 'Berat telur minimal 1 gram!')
|
||||
.typeError('Berat telur harus berupa angka!'),
|
||||
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
||||
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
||||
});
|
||||
|
||||
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
|
||||
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({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
@@ -100,7 +135,7 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
|
||||
.required('Project Flock Kandang wajib diisi!')
|
||||
.test(
|
||||
'not-already-recorded',
|
||||
'Project Flock ini sudah direcord hari ini!',
|
||||
'Project Flock ini sudah direcord pada tanggal tersebut!',
|
||||
function (value) {
|
||||
const recordedProjectFlockIds = this.options.context
|
||||
?.recordedProjectFlockIds as Set<number>;
|
||||
@@ -119,18 +154,12 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
|
||||
.of(StockObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 data stok!')
|
||||
.required('Data stok wajib diisi!'),
|
||||
depletions: Yup.array()
|
||||
.of(DepletionObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 data depletions!')
|
||||
.required('Data depletions wajib diisi!'),
|
||||
depletions: Yup.array().of(DepletionObjectSchema).default([]),
|
||||
});
|
||||
|
||||
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
|
||||
RecordingGrowingFormSchema.shape({
|
||||
eggs: Yup.array()
|
||||
.of(EggObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 data telur!')
|
||||
.required('Data telur wajib diisi!'),
|
||||
eggs: Yup.array().of(EggObjectSchema).default([]),
|
||||
});
|
||||
|
||||
export const UpdateRecordingGrowingFormSchema =
|
||||
@@ -179,6 +208,15 @@ type RecordingFormData = Partial<Recording> & {
|
||||
export const getRecordingGrowingFormInitialValues = (
|
||||
initialValues?: RecordingFormData
|
||||
): 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
|
||||
? {
|
||||
value: initialValues.project_flock_kandang_id,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -179,12 +179,16 @@ const TransferToLayingsTable = () => {
|
||||
setInputValue: setFlockSourceInputValue,
|
||||
options: flockSourceOptions,
|
||||
isLoadingOptions: isLoadingFlockSourceOptions,
|
||||
loadMore: loadMoreFlockSource,
|
||||
hasMore: hasMoreFlockSource,
|
||||
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
setInputValue: setFlockDestinationInputValue,
|
||||
options: flockDestinationOptions,
|
||||
isLoadingOptions: isLoadingFlockDestinationOptions,
|
||||
loadMore: loadMoreFlockDestination,
|
||||
hasMore: hasMoreFlockDestination,
|
||||
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
||||
|
||||
// Flocks value
|
||||
@@ -595,6 +599,7 @@ const TransferToLayingsTable = () => {
|
||||
value={selectedFlockSource}
|
||||
onChange={flockSourceChangeHandler}
|
||||
onInputChange={setFlockSourceInputValue}
|
||||
onMenuScrollToBottom={loadMoreFlockSource}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-3',
|
||||
@@ -608,6 +613,7 @@ const TransferToLayingsTable = () => {
|
||||
value={selectedFlockDestination}
|
||||
onChange={flockDestinationChangeHandler}
|
||||
onInputChange={setFlockDestinationInputValue}
|
||||
onMenuScrollToBottom={loadMoreFlockDestination}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-3',
|
||||
|
||||
@@ -270,6 +270,8 @@ const TransferToLayingForm = ({
|
||||
options: flockSourceOptions,
|
||||
isLoadingOptions: isLoadingFlockSourceOptions,
|
||||
rawData: flockSources,
|
||||
loadMore: loadMoreFlockSource,
|
||||
hasMore: hasMoreFlockSource,
|
||||
} = useSelect<ProjectFlock>(
|
||||
'/production/project-flocks',
|
||||
'id',
|
||||
@@ -360,6 +362,8 @@ const TransferToLayingForm = ({
|
||||
options: flockDestinationOptions,
|
||||
isLoadingOptions: isLoadingFlockDestinationOptions,
|
||||
rawData: flockDestinations,
|
||||
loadMore: loadMoreFlockDestination,
|
||||
hasMore: hasMoreFlockDestination,
|
||||
} = useSelect<ProjectFlock>(
|
||||
'/production/project-flocks',
|
||||
'id',
|
||||
@@ -573,6 +577,7 @@ const TransferToLayingForm = ({
|
||||
onChange={flockSourceChangeHandler}
|
||||
isLoading={isLoadingFlockSourceOptions}
|
||||
onInputChange={setFlockSourceInputValue}
|
||||
onMenuScrollToBottom={loadMoreFlockSource}
|
||||
isError={
|
||||
formik.touched.flockSource &&
|
||||
Boolean(typeof formik.errors.flockSource === 'string')
|
||||
@@ -591,6 +596,7 @@ const TransferToLayingForm = ({
|
||||
onChange={flockDestinationChangeHandler}
|
||||
isLoading={isLoadingFlockDestinationOptions}
|
||||
onInputChange={setFlockDestinationInputValue}
|
||||
onMenuScrollToBottom={loadMoreFlockDestination}
|
||||
isError={
|
||||
formik.touched.flockDestination &&
|
||||
Boolean(typeof formik.errors.flockDestination === 'string')
|
||||
|
||||
@@ -37,7 +37,10 @@ import DateInput from '@/components/input/DateInput';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
import { ProjectFlockApi } from '@/services/api/production';
|
||||
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 {
|
||||
getStatusColor,
|
||||
getStatusIndicatorColor,
|
||||
@@ -229,63 +232,37 @@ const UniformityTable = () => {
|
||||
useState<number | undefined>(undefined);
|
||||
const [filterStartDate, setFilterStartDate] = useState('');
|
||||
const [filterEndDate, setFilterEndDate] = useState('');
|
||||
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
|
||||
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
|
||||
useState<string>('');
|
||||
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const {
|
||||
setInputValue: setFilterLocationInputValue,
|
||||
options: filterLocationOptions,
|
||||
isLoadingOptions: isLoadingFilterLocations,
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
||||
limit: '100',
|
||||
});
|
||||
loadMore: loadMoreFilterLocations,
|
||||
hasMore: hasMoreFilterLocations,
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
// ===== 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 {
|
||||
data: filterProjectFlocksData,
|
||||
isLoading: isLoadingFilterProjectFlocks,
|
||||
} = useSWR(filterProjectFlocksUrl, ProjectFlockApi.getAllFetcher);
|
||||
|
||||
const filterProjectFlocksDataList = useMemo(
|
||||
() =>
|
||||
isResponseSuccess(filterProjectFlocksData)
|
||||
? filterProjectFlocksData.data
|
||||
: 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]);
|
||||
setInputValue: setFilterProjectFlockSearchValue,
|
||||
options: filterProjectFlockOptions,
|
||||
rawData: filterProjectFlocksRawData,
|
||||
isLoadingOptions: isLoadingFilterProjectFlocks,
|
||||
loadMore: loadMoreFilterProjectFlocks,
|
||||
hasMore: hasMoreFilterProjectFlocks,
|
||||
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
|
||||
location_id: filterProjectFlockLocationId,
|
||||
});
|
||||
|
||||
// ===== KANDANG OPTIONS FOR FILTER =====
|
||||
const filterKandangOptions = useMemo(() => {
|
||||
let options: OptionType[] = [];
|
||||
|
||||
if (filterProjectFlock && filterProjectFlocksDataList) {
|
||||
const selectedProjectFlockData = filterProjectFlocksDataList.find(
|
||||
if (filterProjectFlock && isResponseSuccess(filterProjectFlocksRawData)) {
|
||||
const data = filterProjectFlocksRawData.data as unknown as ProjectFlock[];
|
||||
const selectedProjectFlockData = data.find(
|
||||
(pf) => pf.id === filterProjectFlock.value
|
||||
);
|
||||
|
||||
@@ -301,7 +278,7 @@ const UniformityTable = () => {
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [filterProjectFlock, filterProjectFlocksDataList]);
|
||||
}, [filterProjectFlock, filterProjectFlocksRawData]);
|
||||
|
||||
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||
@@ -394,9 +371,13 @@ const UniformityTable = () => {
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterLocationChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
setFilterLocation(val as OptionType | null);
|
||||
const location = val as OptionType | null;
|
||||
setFilterLocation(location);
|
||||
setFilterProjectFlock(null);
|
||||
setFilterKandang(null);
|
||||
setFilterProjectFlockLocationId(
|
||||
location ? location.value.toString() : ''
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
@@ -1206,6 +1187,7 @@ const UniformityTable = () => {
|
||||
options={filterLocationOptions}
|
||||
onInputChange={setFilterLocationInputValue}
|
||||
isLoading={isLoadingFilterLocations}
|
||||
onMenuScrollToBottom={loadMoreFilterLocations}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
{filterErrors.location && (
|
||||
@@ -1225,8 +1207,9 @@ const UniformityTable = () => {
|
||||
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
|
||||
}}
|
||||
options={filterProjectFlockOptions}
|
||||
onInputChange={setProjectFlockSearchValue}
|
||||
onInputChange={setFilterProjectFlockSearchValue}
|
||||
isLoading={isLoadingFilterProjectFlocks}
|
||||
onMenuScrollToBottom={loadMoreFilterProjectFlocks}
|
||||
isDisabled={!filterLocation}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Badge from '../../../../Badge';
|
||||
import Badge from '@/components/Badge';
|
||||
import Card from '@/components/Card';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
|
||||
@@ -36,7 +36,10 @@ import {
|
||||
VerifyUniformityPayload,
|
||||
} from '@/types/api/production/uniformity';
|
||||
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 UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
||||
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
||||
@@ -88,7 +91,9 @@ const UniformityForm = ({
|
||||
null
|
||||
);
|
||||
|
||||
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
|
||||
const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] =
|
||||
useState<string>('');
|
||||
|
||||
const [selectedProjectFlock, setSelectedProjectFlock] =
|
||||
useState<OptionType | null>(null);
|
||||
|
||||
@@ -100,50 +105,21 @@ const UniformityForm = ({
|
||||
setInputValue: setLocationSelectInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocations,
|
||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
||||
page: '1',
|
||||
limit: '100',
|
||||
loadMore: loadMoreLocations,
|
||||
hasMore: hasMoreLocations,
|
||||
} = 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 =====
|
||||
const approvedProjectFlockKandangsUrl = useMemo(() => {
|
||||
const params = new URLSearchParams({
|
||||
@@ -168,8 +144,9 @@ const UniformityForm = ({
|
||||
const kandangOptions = useMemo(() => {
|
||||
let options: OptionType[] = [];
|
||||
|
||||
if (selectedProjectFlock && projectFlocksDataList) {
|
||||
const selectedProjectFlockData = projectFlocksDataList.find(
|
||||
if (selectedProjectFlock && isResponseSuccess(projectFlocksRawData)) {
|
||||
const data = projectFlocksRawData.data as unknown as ProjectFlock[];
|
||||
const selectedProjectFlockData = data.find(
|
||||
(pf) => pf.id === selectedProjectFlock.value
|
||||
);
|
||||
|
||||
@@ -196,7 +173,7 @@ const UniformityForm = ({
|
||||
return options;
|
||||
}, [
|
||||
selectedProjectFlock,
|
||||
projectFlocksDataList,
|
||||
projectFlocksRawData,
|
||||
approvedProjectFlockKandangs,
|
||||
formType,
|
||||
]);
|
||||
@@ -313,6 +290,10 @@ const UniformityForm = ({
|
||||
formik.setFieldValue('location_id', locationId);
|
||||
|
||||
setSelectedLocation(location);
|
||||
setSelectedProjectFlock(null);
|
||||
setSelectedProjectFlockLocationId(
|
||||
location ? location.value.toString() : ''
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
@@ -513,6 +494,7 @@ const UniformityForm = ({
|
||||
options={locationOptions}
|
||||
onInputChange={setLocationSelectInputValue}
|
||||
isLoading={isLoadingLocations}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isError={
|
||||
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||
}
|
||||
@@ -530,6 +512,7 @@ const UniformityForm = ({
|
||||
options={projectFlockOptions}
|
||||
onInputChange={setProjectFlockSearchValue}
|
||||
isLoading={isLoadingProjectFlocks}
|
||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||
isDisabled={!formik.values.location_id}
|
||||
isError={
|
||||
formik.touched.project_flock_id &&
|
||||
|
||||
@@ -156,8 +156,11 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
setInputValue: setExpeditionsSelectInputValue,
|
||||
options: expeditionVendors,
|
||||
isLoadingOptions: isLoadingExpeditions,
|
||||
loadMore: loadMoreExpeditions,
|
||||
hasMore: hasMoreExpeditions,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
|
||||
category: 'BOP',
|
||||
flag: 'EKSPEDISI',
|
||||
});
|
||||
|
||||
// ===== FORM CONFIGURATION =====
|
||||
@@ -183,8 +186,8 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
purchase_item_id: formItem.purchase_item_id || 0,
|
||||
received_date: formItem.received_date || '',
|
||||
travel_number: formItem.travel_number || '',
|
||||
vehicle_number: formItem.vehicle_number || '',
|
||||
expedition_vendor_id: formItem.expedition_vendor_id || 0,
|
||||
vehicle_number: formItem.vehicle_number || null,
|
||||
expedition_vendor_id: formItem.expedition_vendor_id || null,
|
||||
received_qty:
|
||||
typeof formItem.received_qty === 'string'
|
||||
? parseFloat(formItem.received_qty) || 0
|
||||
@@ -192,10 +195,13 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
transport_per_item:
|
||||
typeof formItem.transport_per_item === 'string'
|
||||
? 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) {
|
||||
@@ -403,22 +409,13 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
Dokumen Surat Jalan
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Nomor Kendaraan
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Vendor Ekspedisi
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>Nomor Kendaraan</th>
|
||||
<th>Vendor Ekspedisi</th>
|
||||
<th>
|
||||
Jumlah Diterima
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Transport/Item
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>Transport/Item</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -536,7 +533,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
name={`items.${idx}.vehicle_number`}
|
||||
type='text'
|
||||
value={formItem?.vehicle_number || ''}
|
||||
@@ -562,7 +558,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
</td>
|
||||
<td>
|
||||
<SelectInput
|
||||
required
|
||||
isClearable={true}
|
||||
value={formItem?.expedition_vendor}
|
||||
key={`expedition-vendor-${idx}`}
|
||||
@@ -570,6 +565,8 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
expeditionVendorChangeHandler(idx, val)
|
||||
}
|
||||
options={getExpeditionVendorOptions()}
|
||||
isLoading={isLoadingExpeditions}
|
||||
onMenuScrollToBottom={loadMoreExpeditions}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'expedition_vendor_id')
|
||||
.isError
|
||||
@@ -629,7 +626,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`items.${idx}.transport_per_item`}
|
||||
value={formItem?.transport_per_item || ''}
|
||||
onChange={(e) =>
|
||||
@@ -680,7 +676,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
|
||||
<div className={'col-span-2 my-2'}>
|
||||
<FileInput
|
||||
required
|
||||
name='travel_documents'
|
||||
label='Dokumen Surat Jalan'
|
||||
accept='.pdf,.jpg,.jpeg,.png'
|
||||
|
||||
@@ -38,16 +38,16 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
|
||||
purchase_item_id: number;
|
||||
received_date: string;
|
||||
travel_number: string;
|
||||
vehicle_number: string;
|
||||
vehicle_number?: string | null;
|
||||
expedition_vendor?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
expedition_vendor_id: number;
|
||||
expedition_vendor_id?: number | null;
|
||||
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 = {
|
||||
@@ -75,14 +75,14 @@ export type PurchaseAcceptApprovalItemSchema = {
|
||||
purchase_item_id: number;
|
||||
received_date: string;
|
||||
travel_number: string;
|
||||
vehicle_number: string;
|
||||
vehicle_number?: string | null;
|
||||
expedition_vendor?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
expedition_vendor_id: number;
|
||||
expedition_vendor_id?: number | null;
|
||||
received_qty: number | string;
|
||||
transport_per_item: number | string;
|
||||
transport_per_item?: number | string | null;
|
||||
};
|
||||
|
||||
export type PurchaseDeleteItemsSchema = {
|
||||
@@ -184,24 +184,19 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
||||
.required('No. Surat jalan wajib diisi!')
|
||||
.typeError('No. Surat jalan wajib diisi!'),
|
||||
vehicle_number: Yup.string()
|
||||
.required('Nomor kendaraan wajib diisi!')
|
||||
.typeError('Nomor kendaraan wajib diisi!'),
|
||||
.nullable()
|
||||
.optional()
|
||||
.typeError('Nomor kendaraan harus berupa plat nomor!'),
|
||||
expedition_vendor: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
expedition_vendor_id: Yup.number()
|
||||
.min(1, 'Vendor ekspedisi wajib diisi!')
|
||||
.required('Vendor ekspedisi wajib diisi!')
|
||||
.test(
|
||||
'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!'),
|
||||
.nullable()
|
||||
.optional()
|
||||
.typeError('Vendor ekspedisi harus berupa angka!'),
|
||||
received_qty: Yup.mixed<string | number>()
|
||||
.required('Jumlah diterima wajib diisi!')
|
||||
.test(
|
||||
@@ -217,13 +212,14 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
||||
)
|
||||
.typeError('Jumlah diterima harus berupa angka!'),
|
||||
transport_per_item: Yup.mixed<string | number>()
|
||||
.required('Biaya transport per item wajib diisi!')
|
||||
.nullable()
|
||||
.optional()
|
||||
.test(
|
||||
'is-valid-transport-per-item',
|
||||
'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!',
|
||||
function (value) {
|
||||
if (value === '' || value === null || value === undefined)
|
||||
return false;
|
||||
return true;
|
||||
const numValue =
|
||||
typeof value === 'string' ? parseFloat(value) : value;
|
||||
return !isNaN(numValue) && numValue >= 0;
|
||||
@@ -389,16 +385,17 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseR
|
||||
travel_documents: Yup.array()
|
||||
.of(
|
||||
Yup.mixed<File>()
|
||||
.required('Dokumen surat jalan wajib diupload!')
|
||||
.nullable()
|
||||
.optional()
|
||||
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
|
||||
if (!value) return true;
|
||||
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
||||
return true;
|
||||
})
|
||||
)
|
||||
.required('Dokumen surat jalan wajib diupload!')
|
||||
.min(1, 'Minimal upload 1 dokumen surat jalan!')
|
||||
.typeError('Dokumen surat jalan wajib diupload!'),
|
||||
.nullable()
|
||||
.optional()
|
||||
.typeError('Dokumen surat jalan harus berupa array!'),
|
||||
});
|
||||
|
||||
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
|
||||
|
||||
@@ -633,9 +633,19 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
|
||||
formik.setFieldValue(`items.${idx}.qty`, numValue);
|
||||
|
||||
formik.setFieldValue(`items.${idx}.price`, '');
|
||||
if (
|
||||
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') {
|
||||
const numValue =
|
||||
@@ -1184,8 +1194,10 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
color='warning'
|
||||
className='px-4'
|
||||
onClick={() => {
|
||||
if (type === 'add') {
|
||||
formik.setValues(formikInitialValues);
|
||||
formik.resetForm();
|
||||
}
|
||||
setPurchaseOrderFormErrorMessage('');
|
||||
onCancel?.();
|
||||
onModalClose?.();
|
||||
|
||||
@@ -63,11 +63,9 @@ const PurchaseRequestForm = ({
|
||||
useState('');
|
||||
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
||||
|
||||
// ===== TYPE DEFINITIONS =====
|
||||
interface ProductOptionType {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
const [selectedArea, setSelectedArea] = useState('');
|
||||
const [selectedLocation, setSelectedLocation] = useState('');
|
||||
const [disabledLocation, setDisabledLocation] = useState(true);
|
||||
|
||||
// ===== UTILITY FUNCTIONS =====
|
||||
const isRepeaterInputError = (
|
||||
@@ -160,11 +158,35 @@ const PurchaseRequestForm = ({
|
||||
isLoadingOptions: isLoadingAreas,
|
||||
} = 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 {
|
||||
inputValue: warehouseSelectInputValue,
|
||||
setInputValue: setWarehouseSelectInputValue,
|
||||
options: warehouseOptions,
|
||||
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 =====
|
||||
const formikInitialValues = useMemo<PurchaseRequestFormValues>(
|
||||
@@ -267,70 +289,6 @@ const PurchaseRequestForm = ({
|
||||
return data;
|
||||
}, [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 newItems = [
|
||||
...(formik.values.items || []),
|
||||
@@ -407,6 +365,18 @@ const PurchaseRequestForm = ({
|
||||
}
|
||||
}, [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 =====
|
||||
const handleSupplierChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
@@ -445,6 +415,16 @@ const PurchaseRequestForm = ({
|
||||
formik.setFieldValue('area_id', (area as OptionType)?.value || 0);
|
||||
formik.setFieldTouched('area', true);
|
||||
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.setFieldTouched('location', true);
|
||||
formik.setFieldValue('location', location);
|
||||
|
||||
setSelectedLocation((location as OptionType)?.value as string);
|
||||
},
|
||||
[]
|
||||
);
|
||||
@@ -596,10 +578,15 @@ const PurchaseRequestForm = ({
|
||||
placeholder='Pilih Lokasi...'
|
||||
value={formik.values.location}
|
||||
onChange={handleLocationChange}
|
||||
options={locationOptions}
|
||||
options={
|
||||
selectedArea != '' || initialValues?.area?.id
|
||||
? locationOptions
|
||||
: []
|
||||
}
|
||||
onInputChange={setLocationSelectInputValue}
|
||||
isLoading={isLoadingLocations}
|
||||
isDisabled={type === 'detail'}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isDisabled={type === 'detail' || disabledLocation}
|
||||
isClearable={type !== 'detail'}
|
||||
/>
|
||||
|
||||
@@ -713,6 +700,7 @@ const PurchaseRequestForm = ({
|
||||
options={warehouseOptions}
|
||||
onInputChange={setWarehouseSelectInputValue}
|
||||
isLoading={isLoadingWarehouses}
|
||||
onMenuScrollToBottom={loadMoreWarehouses}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'warehouse_id').isError
|
||||
}
|
||||
@@ -732,9 +720,9 @@ const PurchaseRequestForm = ({
|
||||
required
|
||||
value={item.product ?? undefined}
|
||||
onChange={(val) => {
|
||||
const product = val as ProductOptionType | null;
|
||||
const product = val as OptionType | null;
|
||||
const productId =
|
||||
(product as ProductOptionType)?.value || 0;
|
||||
(product as OptionType)?.value || 0;
|
||||
|
||||
formik.setFieldTouched(
|
||||
`items.${idx}.product`,
|
||||
|
||||
@@ -540,31 +540,6 @@ const PurchaseOrderDetail = ({
|
||||
accessorKey: 'travel_number',
|
||||
cell: (props) => props.row.original.travel_number || '-',
|
||||
},
|
||||
{
|
||||
header: 'Dokumen Surat Jalan',
|
||||
accessorKey: 'travel_document_path',
|
||||
cell: (props) => {
|
||||
const documentPath = props.row.original.travel_document_path;
|
||||
return documentPath ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm'
|
||||
href={documentPath}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:file-open-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Lihat Dokumen
|
||||
</Button>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'No. Armada Pengangkut',
|
||||
accessorKey: 'vehicle_number',
|
||||
@@ -588,7 +563,10 @@ const PurchaseOrderDetail = ({
|
||||
{
|
||||
header: 'Transport /Item',
|
||||
accessorKey: 'transport_per_item',
|
||||
cell: (props) => formatCurrency(props.getValue() as number),
|
||||
cell: (props) => {
|
||||
const value = props.row.original.transport_per_item;
|
||||
return value ? formatCurrency(value) : formatCurrency(0);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -723,8 +701,8 @@ const PurchaseOrderDetail = ({
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
:{' '}
|
||||
{purchaseData.items?.[0]?.warehouse?.type === 'LOKASI' &&
|
||||
purchaseData.items?.[0]?.warehouse?.location?.name
|
||||
{purchaseData.items?.[0]?.warehouse &&
|
||||
'location' in purchaseData.items[0].warehouse
|
||||
? purchaseData.items[0].warehouse.location.name
|
||||
: '-'}
|
||||
</span>
|
||||
@@ -905,11 +883,29 @@ const PurchaseOrderDetail = ({
|
||||
Informasi Penerimaan Barang
|
||||
</h3>
|
||||
{canShowPenerimaanBarang && (
|
||||
<div className='flex items-center gap-2'>
|
||||
{goodsReceiptItems[0]?.travel_document_path && (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 p-1.5 text-sm'
|
||||
href={goodsReceiptItems[0].travel_document_path}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:file-open-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Lihat Dokumen
|
||||
</Button>
|
||||
)}
|
||||
<RowDropdownOptions isLast2Rows>
|
||||
<PenerimaanBarangDropdown
|
||||
onEdit={penerimaanBarangModal.openModal}
|
||||
/>
|
||||
</RowDropdownOptions>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='overflow-x-auto'>
|
||||
|
||||
@@ -324,12 +324,14 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
||||
PT LUMBUNG TELUR INDONESIA
|
||||
</Text>
|
||||
<Text>
|
||||
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
|
||||
{purchaseData?.items?.[0]?.warehouse &&
|
||||
'location' in purchaseData.items[0].warehouse
|
||||
? purchaseData.items[0].warehouse.location.name
|
||||
: '-'}
|
||||
</Text>
|
||||
<Text>
|
||||
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
|
||||
{purchaseData?.items?.[0]?.warehouse &&
|
||||
'location' in purchaseData.items[0].warehouse
|
||||
? purchaseData.items[0].warehouse.location.address
|
||||
: '-'}
|
||||
</Text>
|
||||
@@ -434,7 +436,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>
|
||||
{item.warehouse?.type === 'LOKASI'
|
||||
{item.warehouse && 'location' in item.warehouse
|
||||
? item.warehouse.location.address
|
||||
: '-'}
|
||||
</Text>
|
||||
|
||||
@@ -87,6 +87,7 @@ const DailyMarketingReportContent = () => {
|
||||
setInputValue: setAreaInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
loadMore: loadMoreAreas,
|
||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
|
||||
|
||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
@@ -101,6 +102,7 @@ const DailyMarketingReportContent = () => {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||
|
||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
@@ -118,6 +120,7 @@ const DailyMarketingReportContent = () => {
|
||||
setInputValue: setWarehouseInputValue,
|
||||
options: warehouseOptions,
|
||||
isLoadingOptions: isLoadingWarehouseOptions,
|
||||
loadMore: loadMoreWarehouses,
|
||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
||||
|
||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
@@ -135,6 +138,7 @@ const DailyMarketingReportContent = () => {
|
||||
setInputValue: setCustomerInputValue,
|
||||
options: customerOptions,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
loadMore: loadMoreCustomers,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
@@ -298,6 +302,7 @@ const DailyMarketingReportContent = () => {
|
||||
value={selectedArea}
|
||||
onChange={areaChangeHandler}
|
||||
onInputChange={setAreaInputValue}
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
@@ -312,6 +317,7 @@ const DailyMarketingReportContent = () => {
|
||||
value={selectedLocation}
|
||||
onChange={locationChangeHandler}
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
@@ -326,6 +332,7 @@ const DailyMarketingReportContent = () => {
|
||||
value={selectedWarehouse}
|
||||
onChange={warehouseChangeHandler}
|
||||
onInputChange={setWarehouseInputValue}
|
||||
onMenuScrollToBottom={loadMoreWarehouses}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
@@ -340,6 +347,7 @@ const DailyMarketingReportContent = () => {
|
||||
value={selectedCustomer}
|
||||
onChange={customerChangeHandler}
|
||||
onInputChange={setCustomerInputValue}
|
||||
onMenuScrollToBottom={loadMoreCustomers}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
|
||||
@@ -168,7 +168,7 @@ const DailyMarketingsTable = ({
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
console.log({ sorting });
|
||||
// console.log({ sorting });
|
||||
|
||||
if (sorting.length === 1) {
|
||||
onFilterByChange(sorting[0].id);
|
||||
|
||||
@@ -26,6 +26,15 @@ import MenuItem from '@/components/menu/MenuItem';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { generateReportExpensePDF } from './pdf/ReportExpenseExport';
|
||||
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 = () => {
|
||||
// ===== STATE MANAGEMENT =====
|
||||
@@ -64,16 +73,33 @@ const ReportExpenseTable = () => {
|
||||
});
|
||||
|
||||
// ===== SELECT OPTIONS =====
|
||||
const { options: optionsLocation, isLoadingOptions: isLoadingLocation } =
|
||||
useSelect(`/master-data/locations`, 'id', 'name');
|
||||
const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } =
|
||||
useSelect(`/master-data/suppliers`, 'id', 'name');
|
||||
const { options: optionsKandang, isLoadingOptions: isLoadingKandang } =
|
||||
useSelect(`/master-data/kandangs`, 'id', 'name', '', {
|
||||
location_id: filterState.location_id,
|
||||
});
|
||||
const { options: optionsNonstock, isLoadingOptions: isLoadingNonstock } =
|
||||
useSelect(`/master-data/nonstocks`, 'id', 'name');
|
||||
const {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||
|
||||
const {
|
||||
setInputValue: setSupplierInputValue,
|
||||
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(
|
||||
() => [
|
||||
@@ -86,31 +112,31 @@ const ReportExpenseTable = () => {
|
||||
// Mendapatkan value option select dari filter state
|
||||
const selectedLocation = useMemo(
|
||||
() =>
|
||||
optionsLocation.find(
|
||||
locationOptions.find(
|
||||
(opt) => String(opt.value) === filterState.location_id
|
||||
) || null,
|
||||
[optionsLocation, filterState.location_id]
|
||||
[locationOptions, filterState.location_id]
|
||||
);
|
||||
const selectedSupplier = useMemo(
|
||||
() =>
|
||||
optionsSupplier.find(
|
||||
supplierOptions.find(
|
||||
(opt) => String(opt.value) === filterState.supplier_id
|
||||
) || null,
|
||||
[optionsSupplier, filterState.supplier_id]
|
||||
[supplierOptions, filterState.supplier_id]
|
||||
);
|
||||
const selectedKandang = useMemo(
|
||||
() =>
|
||||
optionsKandang.find(
|
||||
kandangOptions.find(
|
||||
(opt) => String(opt.value) === filterState.kandang_id
|
||||
) || null,
|
||||
[optionsKandang, filterState.kandang_id]
|
||||
[kandangOptions, filterState.kandang_id]
|
||||
);
|
||||
const selectedNonstock = useMemo(
|
||||
() =>
|
||||
optionsNonstock.find(
|
||||
nonstockOptions.find(
|
||||
(opt) => String(opt.value) === filterState.nonstock_id
|
||||
) || null,
|
||||
[optionsNonstock, filterState.nonstock_id]
|
||||
[nonstockOptions, filterState.nonstock_id]
|
||||
);
|
||||
const selectedCategory = useMemo(
|
||||
() =>
|
||||
@@ -756,38 +782,46 @@ const ReportExpenseTable = () => {
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Lokasi'
|
||||
options={optionsLocation}
|
||||
isLoading={isLoadingLocation}
|
||||
options={locationOptions}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
placeholder='Lokasi'
|
||||
value={selectedLocation}
|
||||
onChange={locationChangeHandler}
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Kandang'
|
||||
options={optionsKandang}
|
||||
isLoading={isLoadingKandang}
|
||||
options={kandangOptions}
|
||||
isLoading={isLoadingKandangOptions}
|
||||
placeholder='Kandang'
|
||||
value={selectedKandang}
|
||||
onChange={kandangChangeHandler}
|
||||
onInputChange={setKandangInputValue}
|
||||
onMenuScrollToBottom={loadMoreKandangs}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Supplier'
|
||||
options={optionsSupplier}
|
||||
isLoading={isLoadingSupplier}
|
||||
options={supplierOptions}
|
||||
isLoading={isLoadingSupplierOptions}
|
||||
placeholder='Supplier'
|
||||
value={selectedSupplier}
|
||||
onChange={supplierChangeHandler}
|
||||
onInputChange={setSupplierInputValue}
|
||||
onMenuScrollToBottom={loadMoreSuppliers}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
label='Produk'
|
||||
options={optionsNonstock}
|
||||
isLoading={isLoadingNonstock}
|
||||
options={nonstockOptions}
|
||||
isLoading={isLoadingNonstockOptions}
|
||||
placeholder='Produk'
|
||||
value={selectedNonstock}
|
||||
onChange={nonstockChangeHandler}
|
||||
onInputChange={setNonstockInputValue}
|
||||
onMenuScrollToBottom={loadMoreNonstocks}
|
||||
/>
|
||||
<SelectInput
|
||||
isClearable
|
||||
|
||||
@@ -177,10 +177,12 @@ interface CustomerPaymentExportPDFParams {
|
||||
data: CustomerPaymentReport[];
|
||||
params?: {
|
||||
customer_name?: string;
|
||||
sales?: string;
|
||||
// TODO: Uncomment when BE is ready
|
||||
// sales?: string;
|
||||
start_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');
|
||||
}
|
||||
|
||||
if (params?.sales) {
|
||||
paramsText.push(`Sales: ${params.sales}`);
|
||||
}
|
||||
// TODO: Uncomment when BE is ready
|
||||
// if (params?.sales) {
|
||||
// paramsText.push(`Sales: ${params.sales}`);
|
||||
// }
|
||||
|
||||
if (params?.start_date && params?.end_date) {
|
||||
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||
@@ -242,9 +245,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
{/* TODO: Uncomment when BE is ready */}
|
||||
{/* <View style={pdfStyles.parameterBadge}>
|
||||
<Text>Filter Tanggal: Tanggal DO</Text>
|
||||
</View>
|
||||
</View> */}
|
||||
<View style={pdfStyles.parameterBadge}>
|
||||
<Text>
|
||||
Customer: {params.params?.customer_name || 'Semua Customer'}
|
||||
@@ -280,7 +284,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.8 }]}>
|
||||
<Text>Aging</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||
<Text>Referensi</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||
@@ -296,17 +300,11 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
<Text>Rata-Rata</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Harga Awal</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>CN</Text>
|
||||
<Text>Harga/Unit</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Harga Akhir</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||
<Text>Pajak</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Total</Text>
|
||||
</View>
|
||||
@@ -343,13 +341,15 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{item.do_date ? formatDate(item.do_date, 'DD MMM YY') : '-'}
|
||||
{item.trans_date
|
||||
? formatDate(item.trans_date, 'DD MMM YY')
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{item.realization_date
|
||||
? formatDate(item.realization_date, 'DD MMM YY')
|
||||
{item.delivery_date
|
||||
? formatDate(item.delivery_date, 'DD MMM YY')
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -358,11 +358,15 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
{item.aging_day ? formatNumber(item.aging_day) : '-'} hari
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||
<Text>{item.reference || '-'}</Text>
|
||||
</View>
|
||||
<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 style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||
<Text>{formatNumber(item.qty)}</Text>
|
||||
@@ -374,22 +378,16 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
<Text>{formatNumber(item.average_weight)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatCurrency(item.credit_note)}</Text>
|
||||
<Text>{formatCurrency(item.unit_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.final_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||
<Text>{formatNumber(item.ppn)}%</Text>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.total_price)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.total)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.payment)}</Text>
|
||||
<Text>{formatCurrency(item.payment_amount)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text style={pdfStyles.textError}>
|
||||
@@ -397,30 +395,32 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||
{item.notes ? (
|
||||
<Text>{item.notes}</Text>
|
||||
) : (
|
||||
{item.status ? (
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.badge,
|
||||
item.accounts_receivable === 0
|
||||
item.status === 'LUNAS'
|
||||
? pdfStyles.badgeLunas
|
||||
: pdfStyles.badgeBelumLunas,
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{item.accounts_receivable === 0
|
||||
? 'Lunas'
|
||||
: 'Belum Lunas'}
|
||||
{item.status === 'LUNAS' ? 'Lunas' : 'Belum Lunas'}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text>-</Text>
|
||||
)}
|
||||
</View>
|
||||
<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 style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||
<Text>{item.sales_marketing || '-'}</Text>
|
||||
<Text>{item.sales_person || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
@@ -440,7 +440,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
<View style={[pdfStyles.tableCell, { flex: 0.8 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||
@@ -458,25 +458,13 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{formatCurrency(
|
||||
customerReport.summary.total_initial_amount
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>
|
||||
{formatCurrency(customerReport.summary.total_credit_note)}
|
||||
</Text>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{formatCurrency(customerReport.summary.total_final_amount)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>
|
||||
{formatCurrency(customerReport.summary.total_grand_amount)}
|
||||
|
||||
@@ -24,30 +24,30 @@ export const generateCustomerPaymentExcel = (
|
||||
const excelData: { [key: string]: string | number }[] = customerData.map(
|
||||
(item, index) => ({
|
||||
No: index + 1,
|
||||
'Tanggal DO/Bayar': item.do_date
|
||||
? formatDate(item.do_date, 'DD MMM YYYY')
|
||||
'Tanggal DO/Bayar': item.trans_date
|
||||
? formatDate(item.trans_date, 'DD MMM YYYY')
|
||||
: '',
|
||||
'Tanggal Realisasi': item.realization_date
|
||||
? formatDate(item.realization_date, 'DD MMM YYYY')
|
||||
'Tanggal Realisasi': item.delivery_date
|
||||
? formatDate(item.delivery_date, 'DD MMM YYYY')
|
||||
: '',
|
||||
Aging: formatNumber(item.aging_day || 0),
|
||||
Referensi: item.reference || '',
|
||||
'Nomor Polisi': Array.isArray(item.vehicle_plate)
|
||||
? item.vehicle_plate.join(', ')
|
||||
'Nomor Polisi': Array.isArray(item.vehicle_numbers)
|
||||
? item.vehicle_numbers.join(', ')
|
||||
: '',
|
||||
'Ekor/Qty': formatNumber(item.qty || 0),
|
||||
'Berat (Kg)': formatNumber(item.weight || 0),
|
||||
AVG: formatNumber(item.average_weight || 0),
|
||||
'Harga Awal': formatCurrency(item.price || 0),
|
||||
CN: formatCurrency(item.credit_note || 0),
|
||||
'Harga/Unit': formatCurrency(item.unit_price || 0),
|
||||
'Harga Akhir': formatCurrency(item.final_price || 0),
|
||||
'PPN (%)': formatNumber(item.ppn || 0),
|
||||
Total: formatCurrency(item.total || 0),
|
||||
Pembayaran: formatCurrency(item.payment || 0),
|
||||
Total: formatCurrency(item.total_price || 0),
|
||||
Pembayaran: formatCurrency(item.payment_amount || 0),
|
||||
'Saldo Piutang': formatCurrency(item.accounts_receivable || 0),
|
||||
Keterangan: item.notes || '',
|
||||
Pengambilan: item.pickup_info || '',
|
||||
'Sales/Marketing': item.sales_marketing || '',
|
||||
Keterangan: item.status || '',
|
||||
Pengambilan: Array.isArray(item.pickup_info)
|
||||
? item.pickup_info.join(', ')
|
||||
: '',
|
||||
'Sales/Marketing': item.sales_person || '',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -62,14 +62,10 @@ export const generateCustomerPaymentExcel = (
|
||||
'Ekor/Qty': formatNumber(customerReport.summary.total_qty || 0),
|
||||
'Berat (Kg)': formatNumber(customerReport.summary.total_weight || 0),
|
||||
AVG: '',
|
||||
'Harga Awal': formatCurrency(
|
||||
customerReport.summary.total_initial_amount || 0
|
||||
),
|
||||
CN: formatCurrency(customerReport.summary.total_credit_note || 0),
|
||||
'Harga/Unit': '',
|
||||
'Harga Akhir': formatCurrency(
|
||||
customerReport.summary.total_final_amount || 0
|
||||
),
|
||||
'PPN (%)': '',
|
||||
Total: formatCurrency(customerReport.summary.total_grand_amount || 0),
|
||||
Pembayaran: formatCurrency(customerReport.summary.total_payment || 0),
|
||||
'Saldo Piutang': formatCurrency(
|
||||
@@ -93,10 +89,8 @@ export const generateCustomerPaymentExcel = (
|
||||
{ wch: 10 }, // Ekor/Qty
|
||||
{ wch: 12 }, // Berat
|
||||
{ wch: 10 }, // AVG
|
||||
{ wch: 15 }, // Harga Awal
|
||||
{ wch: 10 }, // CN
|
||||
{ wch: 15 }, // Harga/Unit
|
||||
{ wch: 15 }, // Harga Akhir
|
||||
{ wch: 10 }, // PPN
|
||||
{ wch: 15 }, // Total
|
||||
{ wch: 15 }, // Pembayaran
|
||||
{ wch: 15 }, // Saldo Piutang
|
||||
|
||||
@@ -187,10 +187,30 @@ const pdfStyles = StyleSheet.create({
|
||||
textAlign: 'center',
|
||||
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 {
|
||||
data: DebtSupplier[];
|
||||
params?: {
|
||||
supplier_name?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
filter_by?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
@@ -208,9 +228,50 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
||||
<Text style={pdfStyles.mainTitle}>
|
||||
Laporan > Rekapitulasi Hutang ke Supplier
|
||||
</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}>
|
||||
{supplierReport.supplier.name}
|
||||
</Text>
|
||||
<Text style={pdfStyles.supplierInfo}>
|
||||
{supplierReport.supplier.category}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Table */}
|
||||
|
||||
@@ -94,18 +94,18 @@ export const generateDebtSupplierExcel = (
|
||||
|
||||
const colWidths = [
|
||||
{ wch: 5 }, // No
|
||||
{ wch: 15 }, // Nomor PR
|
||||
{ wch: 15 }, // Nomor PO
|
||||
{ wch: 15 }, // Tanggal Terima/Bayar
|
||||
{ wch: 15 }, // Tanggal PO
|
||||
{ wch: 12 }, // Aging
|
||||
{ wch: 10 }, // Nomor PR
|
||||
{ wch: 10 }, // Nomor PO
|
||||
{ wch: 20 }, // Tanggal Terima/Bayar
|
||||
{ wch: 10 }, // Tanggal PO
|
||||
{ wch: 10 }, // Aging
|
||||
{ wch: 15 }, // Area
|
||||
{ wch: 15 }, // Gudang
|
||||
{ wch: 18 }, // Jatuh Tempo
|
||||
{ wch: 18 }, // Status Jatuh Tempo
|
||||
{ wch: 15 }, // Nominal Pembelian (Rp)
|
||||
{ wch: 12 }, // Jatuh Tempo
|
||||
{ wch: 20 }, // Status Jatuh Tempo
|
||||
{ wch: 20 }, // Nominal Pembelian (Rp)
|
||||
{ wch: 15 }, // Pembayaran (Rp)
|
||||
{ wch: 15 }, // Sisa Saldo Hutang (Rp)
|
||||
{ wch: 20 }, // Sisa Saldo Hutang (Rp)
|
||||
{ wch: 12 }, // Status
|
||||
{ wch: 15 }, // Nomor Perjalanan
|
||||
];
|
||||
|
||||
@@ -47,6 +47,8 @@ const CustomerPaymentTab = () => {
|
||||
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 [filterStartDate, setFilterStartDate] = useState('');
|
||||
const [filterEndDate, setFilterEndDate] = useState('');
|
||||
@@ -55,13 +57,16 @@ const CustomerPaymentTab = () => {
|
||||
|
||||
const {
|
||||
options: customerOptions,
|
||||
setInputValue: setCustomerInputValue,
|
||||
isLoadingOptions: isLoadingCustomers,
|
||||
loadMore: loadMoreCustomers,
|
||||
hasMore: hasMoreCustomers,
|
||||
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
// TODO: Uncomment when BE is ready
|
||||
const {
|
||||
options: salesOptions,
|
||||
setInputValue: setSalesInputValue,
|
||||
isLoadingOptions: isLoadingSales,
|
||||
loadMore: loadMoreSales,
|
||||
hasMore: hasMoreSales,
|
||||
@@ -101,7 +106,11 @@ const CustomerPaymentTab = () => {
|
||||
};
|
||||
|
||||
const getPaymentStatusText = (notes: string) => {
|
||||
return notes;
|
||||
return notes
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
@@ -133,23 +142,18 @@ const CustomerPaymentTab = () => {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Sales filter
|
||||
if (filterSales.length > 0) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Filter by (always count if submitted)
|
||||
if (isSubmitted) {
|
||||
count += 1;
|
||||
}
|
||||
// TODO: Uncomment when BE is ready
|
||||
// // Sales filter
|
||||
// if (filterSales.length > 0) {
|
||||
// count += 1;
|
||||
// }
|
||||
|
||||
return count;
|
||||
}, [
|
||||
filterStartDate,
|
||||
filterEndDate,
|
||||
filterCustomer,
|
||||
filterSales,
|
||||
isSubmitted,
|
||||
// filterSales,
|
||||
]);
|
||||
|
||||
const hasFilters = activeFiltersCount > 0;
|
||||
@@ -159,15 +163,16 @@ const CustomerPaymentTab = () => {
|
||||
isSubmitted
|
||||
? () => {
|
||||
const params = {
|
||||
customer_id:
|
||||
customer_ids:
|
||||
filterCustomer.length > 0
|
||||
? filterCustomer.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
sales_id:
|
||||
filterSales.length > 0
|
||||
? filterSales.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
filter_by: 'do_date' as const,
|
||||
// TODO: Uncomment when BE is ready
|
||||
// sales_id:
|
||||
// filterSales.length > 0
|
||||
// ? filterSales.map((v) => String(v.value)).join(',')
|
||||
// : undefined,
|
||||
// filter_by: 'do_date' as const,
|
||||
start_date: filterStartDate || undefined,
|
||||
end_date: filterEndDate || undefined,
|
||||
page: currentPage,
|
||||
@@ -179,9 +184,9 @@ const CustomerPaymentTab = () => {
|
||||
: null,
|
||||
([, params]) =>
|
||||
FinanceApi.getCustomerPaymentReport(
|
||||
params.customer_id,
|
||||
params.sales_id,
|
||||
params.filter_by,
|
||||
params.customer_ids,
|
||||
undefined, // TODO: Change to params.sales_id when BE is ready
|
||||
undefined, // TODO: Change to params.filter_by when BE is ready
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
params.page,
|
||||
@@ -202,15 +207,15 @@ const CustomerPaymentTab = () => {
|
||||
CustomerPaymentReport[] | null
|
||||
> => {
|
||||
const params = {
|
||||
customer_id:
|
||||
customer_ids:
|
||||
filterCustomer.length > 0
|
||||
? filterCustomer.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
sales_id:
|
||||
filterSales.length > 0
|
||||
? filterSales.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
filter_by: 'do_date' as const,
|
||||
// TODO: Uncomment when BE is ready
|
||||
// sales_id:
|
||||
// filterSales.length > 0
|
||||
// ? filterSales.map((v) => String(v.value)).join(',')
|
||||
// : undefined,
|
||||
start_date: filterStartDate || undefined,
|
||||
end_date: filterEndDate || undefined,
|
||||
limit: 100,
|
||||
@@ -218,9 +223,9 @@ const CustomerPaymentTab = () => {
|
||||
};
|
||||
|
||||
const response = await FinanceApi.getCustomerPaymentReport(
|
||||
params.customer_id,
|
||||
params.sales_id,
|
||||
params.filter_by,
|
||||
params.customer_ids,
|
||||
undefined, // TODO: Change to params.sales_id when BE is ready
|
||||
undefined, // TODO: Change to params.filter_by when BE is ready
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
params.page,
|
||||
@@ -277,13 +282,15 @@ const CustomerPaymentTab = () => {
|
||||
filterCustomer.length > 0
|
||||
? filterCustomer.map((c) => c.label).join(', ')
|
||||
: undefined,
|
||||
sales:
|
||||
filterSales.length > 0
|
||||
? filterSales.map((s) => s.label).join(', ')
|
||||
: undefined,
|
||||
// TODO: Uncomment when BE is ready
|
||||
// sales:
|
||||
// filterSales.length > 0
|
||||
// ? filterSales.map((s) => s.label).join(', ')
|
||||
// : undefined,
|
||||
start_date: filterStartDate || 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.');
|
||||
@@ -301,36 +308,41 @@ const CustomerPaymentTab = () => {
|
||||
{
|
||||
id: 'no',
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
cell: (props) => props.row.index,
|
||||
footer: () => <div className='font-semibold text-gray-900'>Total</div>,
|
||||
},
|
||||
{
|
||||
id: 'do_date_or_payment_date',
|
||||
header: 'Tanggal DO/Bayar',
|
||||
accessorKey: 'do_date',
|
||||
header: 'Tanggal Jual/Bayar',
|
||||
accessorKey: 'trans_date',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.do_date;
|
||||
return formatDate(value, 'DD MMM YYYY');
|
||||
const value = props.row.original.trans_date;
|
||||
return value ? formatDate(value, 'DD MMM YYYY') : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'realization_date',
|
||||
header: 'Tanggal Realisasi',
|
||||
accessorKey: 'realization_date',
|
||||
accessorKey: 'delivery_date',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.realization_date;
|
||||
return formatDate(value, 'DD MMM YYYY');
|
||||
const value = props.row.original.delivery_date;
|
||||
return value ? formatDate(value, 'DD MMM YYYY') : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'aging',
|
||||
header: 'Aging',
|
||||
accessorKey: 'aging_day',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.aging_day;
|
||||
return (
|
||||
<div className='text-center'>
|
||||
{value ? formatNumber(value) : '-'} hari
|
||||
{value !== null && value !== undefined
|
||||
? `${formatNumber(value)} hari`
|
||||
: '-'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -339,6 +351,7 @@ const CustomerPaymentTab = () => {
|
||||
id: 'reference',
|
||||
header: 'Referensi',
|
||||
accessorKey: 'reference',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.reference;
|
||||
return value || '-';
|
||||
@@ -347,16 +360,18 @@ const CustomerPaymentTab = () => {
|
||||
{
|
||||
id: 'vehicle_plate',
|
||||
header: 'Nomor Polisi',
|
||||
accessorKey: 'vehicle_plate',
|
||||
accessorKey: 'vehicle_numbers',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.vehicle_plate;
|
||||
return value || '-';
|
||||
const value = props.row.original.vehicle_numbers;
|
||||
return Array.isArray(value) ? value.join(', ') : value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'qty',
|
||||
header: 'Ekor/Qty',
|
||||
header: 'Qty',
|
||||
accessorKey: 'qty',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.qty;
|
||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||
@@ -371,6 +386,7 @@ const CustomerPaymentTab = () => {
|
||||
id: 'weight',
|
||||
header: 'Berat (Kg)',
|
||||
accessorKey: 'weight',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.weight;
|
||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||
@@ -385,6 +401,7 @@ const CustomerPaymentTab = () => {
|
||||
id: 'average_weight',
|
||||
header: 'AVG',
|
||||
accessorKey: 'average_weight',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.average_weight;
|
||||
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||
@@ -394,37 +411,23 @@ const CustomerPaymentTab = () => {
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'price',
|
||||
header: 'Harga Awal',
|
||||
accessorKey: 'price',
|
||||
id: 'unit_price',
|
||||
header: 'Harga/Unit',
|
||||
accessorKey: 'unit_price',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.price;
|
||||
const value = props.row.original.unit_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
<div className='text-right font-semibold text-gray-900'>
|
||||
{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>
|
||||
<div className='text-right font-semibold text-gray-900'>-</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'final_price',
|
||||
header: 'Harga Akhir',
|
||||
accessorKey: 'final_price',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.final_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
@@ -435,24 +438,13 @@ const CustomerPaymentTab = () => {
|
||||
</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',
|
||||
header: 'Total',
|
||||
accessorKey: 'total',
|
||||
accessorKey: 'total_price',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.total;
|
||||
const value = props.row.original.total_price;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
@@ -464,9 +456,10 @@ const CustomerPaymentTab = () => {
|
||||
{
|
||||
id: 'payment',
|
||||
header: 'Pembayaran',
|
||||
accessorKey: 'payment',
|
||||
accessorKey: 'payment_amount',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.payment;
|
||||
const value = props.row.original.payment_amount;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
footer: () => (
|
||||
@@ -479,14 +472,25 @@ const CustomerPaymentTab = () => {
|
||||
id: 'accounts_receivable',
|
||||
header: 'Saldo Piutang',
|
||||
accessorKey: 'accounts_receivable',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.accounts_receivable;
|
||||
return (
|
||||
<div className='text-right text-error'>{formatCurrency(value)}</div>
|
||||
<div
|
||||
className={`text-right font-semibold ${
|
||||
value < 0 ? 'text-error' : ''
|
||||
}`}
|
||||
>
|
||||
{formatCurrency(value)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
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) || '-'}
|
||||
</div>
|
||||
),
|
||||
@@ -494,9 +498,10 @@ const CustomerPaymentTab = () => {
|
||||
{
|
||||
id: 'notes',
|
||||
header: 'Keterangan',
|
||||
accessorKey: 'notes',
|
||||
accessorKey: 'status',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.notes;
|
||||
const value = props.row.original.status;
|
||||
|
||||
if (!value) {
|
||||
return '-';
|
||||
@@ -511,7 +516,7 @@ const CustomerPaymentTab = () => {
|
||||
status: getPaymentStatusIndicatorColor(value),
|
||||
}}
|
||||
>
|
||||
{getPaymentStatusText(value)}
|
||||
<span className='capitalize'>{getPaymentStatusText(value)}</span>
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
@@ -520,17 +525,19 @@ const CustomerPaymentTab = () => {
|
||||
id: 'pickup_info',
|
||||
header: 'Pengambilan',
|
||||
accessorKey: 'pickup_info',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.pickup_info;
|
||||
return value || '-';
|
||||
return Array.isArray(value) ? value.join(', ') : value || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sales_marketing',
|
||||
header: 'Sales/Marketing',
|
||||
accessorKey: 'sales_marketing',
|
||||
accessorKey: 'sales_person',
|
||||
enableSorting: false,
|
||||
cell: (props) => {
|
||||
const value = props.row.original.sales_marketing;
|
||||
const value = props.row.original.sales_person;
|
||||
return value || '-';
|
||||
},
|
||||
},
|
||||
@@ -654,6 +661,7 @@ const CustomerPaymentTab = () => {
|
||||
Array.isArray(val) ? val : val ? [val] : []
|
||||
);
|
||||
}}
|
||||
onInputChange={setCustomerInputValue}
|
||||
isLoading={isLoadingCustomers}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreCustomers}
|
||||
@@ -661,7 +669,8 @@ const CustomerPaymentTab = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* TODO: Uncomment when BE is ready */}
|
||||
{/* <div>
|
||||
<SelectInputCheckbox
|
||||
label='Sales'
|
||||
placeholder='Pilih Sales'
|
||||
@@ -670,14 +679,16 @@ const CustomerPaymentTab = () => {
|
||||
onChange={(val) => {
|
||||
setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
|
||||
}}
|
||||
onInputChange={setSalesInputValue}
|
||||
isLoading={isLoadingSales}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreSales}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div>
|
||||
{/* TODO: Uncomment when BE is ready */}
|
||||
{/* <div>
|
||||
<SelectInput
|
||||
label='Filter Berdasarkan'
|
||||
placeholder='Pilih Filter Berdasarkan'
|
||||
@@ -686,7 +697,7 @@ const CustomerPaymentTab = () => {
|
||||
isDisabled={true}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
@@ -726,10 +737,7 @@ const CustomerPaymentTab = () => {
|
||||
const summary = customerReport.summary || {
|
||||
total_qty: 0,
|
||||
total_weight: 0,
|
||||
total_initial_amount: 0,
|
||||
total_credit_note: 0,
|
||||
total_final_amount: 0,
|
||||
total_ppn: 0,
|
||||
total_grand_amount: 0,
|
||||
total_payment: 0,
|
||||
total_accounts_receivable: 0,
|
||||
@@ -741,19 +749,27 @@ const CustomerPaymentTab = () => {
|
||||
<Card
|
||||
key={customerReport.customer.id}
|
||||
title={customerReport.customer.name}
|
||||
subtitle={`(${customerReport.customer.address})`}
|
||||
className={{
|
||||
wrapper: 'w-full rounded-2xl',
|
||||
body: 'p-0',
|
||||
title:
|
||||
'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'
|
||||
collapsible={true}
|
||||
>
|
||||
<Table
|
||||
data={customerReport.rows}
|
||||
data={[
|
||||
{
|
||||
accounts_receivable: customerReport.initial_balance,
|
||||
} as CustomerPaymentReport['rows'][0],
|
||||
...customerReport.rows,
|
||||
]}
|
||||
columns={tableColumns}
|
||||
pageSize={10}
|
||||
pageSize={customerReport.rows.length + 1}
|
||||
renderFooter={customerReport.rows.length > 0}
|
||||
className={{
|
||||
containerClassName: 'w-full',
|
||||
@@ -773,6 +789,36 @@ const CustomerPaymentTab = () => {
|
||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
import Badge from '@/components/Badge';
|
||||
import { Color } from '@/types/theme';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
|
||||
const dueStatus: Record<string, Color> = {
|
||||
'Sudah Jatuh Tempo': 'error',
|
||||
@@ -89,10 +90,12 @@ const DebtSupplierTab = () => {
|
||||
|
||||
const filterModal = useModal();
|
||||
|
||||
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
|
||||
useSelect(SupplierApi.basePath, 'id', 'name', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
const {
|
||||
setInputValue: setSupplierInputValue,
|
||||
options: supplierOptions,
|
||||
isLoadingOptions: isLoadingSupplierOptions,
|
||||
loadMore: loadMoreSuppliers,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||
|
||||
const dataTypeOptions = useMemo(
|
||||
() => [
|
||||
@@ -247,7 +250,17 @@ const DebtSupplierTab = () => {
|
||||
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.');
|
||||
} catch {
|
||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||
@@ -670,7 +683,9 @@ const DebtSupplierTab = () => {
|
||||
Array.isArray(val) ? val : val ? [val] : null
|
||||
);
|
||||
}}
|
||||
isLoading={isLoadingSuppliers}
|
||||
onInputChange={setSupplierInputValue}
|
||||
onMenuScrollToBottom={loadMoreSuppliers}
|
||||
isLoading={isLoadingSupplierOptions}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
isError={
|
||||
|
||||
@@ -21,10 +21,18 @@ import {
|
||||
ProjectFlockApi,
|
||||
ProjectFlockKandangApi,
|
||||
} from '@/services/api/production';
|
||||
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import {
|
||||
BaseProjectFlockKandang,
|
||||
ProjectFlockKandang,
|
||||
} from '@/types/api/production/project-flock-kandang';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import Pagination from '@/components/Pagination';
|
||||
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 [projectFlockKandangs, setProjectFlockKandangs] = useState<
|
||||
@@ -49,6 +57,8 @@ const ProductionResultContent = () => {
|
||||
const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] =
|
||||
useState(false);
|
||||
|
||||
const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false);
|
||||
|
||||
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||
null
|
||||
@@ -62,6 +72,7 @@ const ProductionResultContent = () => {
|
||||
setInputValue: setAreaInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
loadMore: loadMoreAreas,
|
||||
} = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name');
|
||||
|
||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
@@ -78,6 +89,7 @@ const ProductionResultContent = () => {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', {
|
||||
area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '',
|
||||
});
|
||||
@@ -94,6 +106,7 @@ const ProductionResultContent = () => {
|
||||
setInputValue: setProjectFlockInputValue,
|
||||
options: projectFlockOptions,
|
||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||
loadMore: loadMoreProjectFlocks,
|
||||
} = useSelect<BaseKandang>(
|
||||
ProjectFlockApi.basePath,
|
||||
'id',
|
||||
@@ -120,6 +133,7 @@ const ProductionResultContent = () => {
|
||||
setInputValue: setProjectFlockKandangInputValue,
|
||||
options: projectFlockKandangOptions,
|
||||
isLoadingOptions: isLoadingProjectFlockKandangOptions,
|
||||
loadMore: loadMoreProjectFlockKandangs,
|
||||
} = useSelect<BaseKandang>(
|
||||
ProjectFlockKandangApi.basePath,
|
||||
'id',
|
||||
@@ -154,6 +168,87 @@ const ProductionResultContent = () => {
|
||||
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 () => {
|
||||
setProjectFlockKandangs(null);
|
||||
setIsLoadingSearch(true);
|
||||
@@ -235,6 +330,7 @@ const ProductionResultContent = () => {
|
||||
value={selectedArea}
|
||||
onChange={areaChangeHandler}
|
||||
onInputChange={setAreaInputValue}
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||
@@ -251,6 +347,7 @@ const ProductionResultContent = () => {
|
||||
value={selectedLocation}
|
||||
onChange={locationChangeHandler}
|
||||
onInputChange={setLocationInputValue}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isClearable
|
||||
isDisabled={!selectedArea}
|
||||
className={{
|
||||
@@ -270,6 +367,7 @@ const ProductionResultContent = () => {
|
||||
value={selectedProjectFlock}
|
||||
onChange={projectFlockChangeHandler}
|
||||
onInputChange={setProjectFlockInputValue}
|
||||
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||
isClearable
|
||||
isDisabled={!selectedArea || !selectedLocation}
|
||||
className={{
|
||||
@@ -289,6 +387,7 @@ const ProductionResultContent = () => {
|
||||
value={selectedProjectFlockKandang}
|
||||
onChange={projectFlockKandangChangeHandler}
|
||||
onInputChange={setProjectFlockKandangInputValue}
|
||||
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
|
||||
isClearable
|
||||
isDisabled={!selectedProjectFlock}
|
||||
className={{
|
||||
@@ -347,6 +446,13 @@ const ProductionResultContent = () => {
|
||||
onClick={exportToExcelHandler}
|
||||
className='text-nowrap'
|
||||
/>
|
||||
<MenuItem
|
||||
title='Export to PDF'
|
||||
icon='icon-park-outline:file-pdf-one'
|
||||
isLoading={isLoadingExportingToPdf}
|
||||
onClick={exportToPdfHandler}
|
||||
className='text-nowrap'
|
||||
/>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</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>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>Sisa Ekor</Text>
|
||||
<Text>Sisa Butir</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||
<Text>Sisa Kg</Text>
|
||||
@@ -234,12 +234,6 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||
</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 }]}>
|
||||
<Text>Feed (Supplier)</Text>
|
||||
</View>
|
||||
@@ -249,16 +243,15 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Rata-Rata Harga DOC</Text>
|
||||
</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 }]}>
|
||||
<Text>HPP Telur (RP/KG)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{ flex: 1.2, borderRightWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text>Nominal Sisa</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -278,23 +271,15 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
||||
<Text>{group.label}</Text>
|
||||
</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 }]}>
|
||||
<Text>{formatNumber(group.egg_production_pieces)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatNumber(group.egg_production_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatNumber(group.avg_weight_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||
<Text>
|
||||
{group.feed_suppliers
|
||||
@@ -318,17 +303,16 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(group.average_doc_price_rp)}</Text>
|
||||
</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 }]}>
|
||||
<Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(group.remaining_value_rp)}</Text>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{ flex: 1.2, borderRightWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text>{formatCurrency(group.egg_value_rp)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
@@ -356,16 +340,10 @@ const createPDFDocument = (
|
||||
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||
<Text>Sisa Ekor</Text>
|
||||
<Text>Sisa Butir</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||
<Text>Sisa Kg (Ayam)</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>
|
||||
<Text>Sisa Kg (Telur)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||
<Text>Feed (Supplier)</Text>
|
||||
@@ -376,16 +354,15 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<Text>Rata-Rata Harga DOC</Text>
|
||||
</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 }]}>
|
||||
<Text>HPP Telur (RP/KG)</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{ flex: 1.2, borderRightWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text>Nominal Sisa</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -394,12 +371,7 @@ const createPDFDocument = (
|
||||
{data.rows.map((item: HppPerKandangRow, index: number) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
index < data.rows.length - 1
|
||||
? pdfStyles.tableBorderBottom
|
||||
: {},
|
||||
]}
|
||||
style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}
|
||||
>
|
||||
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
|
||||
<Text>{index + 1}</Text>
|
||||
@@ -416,12 +388,6 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||
<Text>{formatNumber(item.avg_weight_kg)}</Text>
|
||||
</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 }]}>
|
||||
<Text>{formatNumber(item.egg_production_pieces)}</Text>
|
||||
</View>
|
||||
@@ -451,20 +417,202 @@ const createPDFDocument = (
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.average_doc_price_rp)}</Text>
|
||||
</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 }]}>
|
||||
<Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||
<Text>{formatCurrency(item.remaining_value_rp)}</Text>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRight,
|
||||
{ flex: 1.2, borderRightWidth: 0 },
|
||||
]}
|
||||
>
|
||||
<Text>{formatCurrency(item.egg_value_rp)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* TOTAL Row */}
|
||||
{data.summary?.total && (
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeader,
|
||||
{
|
||||
flex: 0.5,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>TOTAL</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeader,
|
||||
{
|
||||
flex: 1.5,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>ALL</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeader,
|
||||
{
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>-</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{formatNumber(data.summary.total.average_weight_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{
|
||||
flex: 0.8,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{formatNumber(
|
||||
data.summary.total.total_egg_production_pieces
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{
|
||||
flex: 0.8,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{formatNumber(data.summary.total.total_egg_production_kg)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeader,
|
||||
{
|
||||
flex: 1.2,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{data.rows
|
||||
.flatMap((row: HppPerKandangRow) =>
|
||||
row.feed_suppliers?.map(
|
||||
(s: { alias?: string; name: string }) =>
|
||||
s.alias || s.name
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
(v: string, i: number, a: string[]) =>
|
||||
a.indexOf(v) === i
|
||||
)
|
||||
.join(' | ') || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeader,
|
||||
{
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{data.rows
|
||||
.flatMap((row: HppPerKandangRow) =>
|
||||
row.doc_suppliers?.map(
|
||||
(s: { alias?: string; name: string }) =>
|
||||
s.alias || s.name
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
(v: string, i: number, a: string[]) =>
|
||||
a.indexOf(v) === i
|
||||
)
|
||||
.join(' | ') || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{
|
||||
flex: 1.2,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{formatCurrency(
|
||||
data.summary.total.total_average_doc_price_rp
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{
|
||||
flex: 1,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{formatCurrency(
|
||||
data.summary.total.average_egg_hpp_rp_per_kg
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellHeaderRight,
|
||||
{
|
||||
flex: 1.2,
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderBottomWidth: 0,
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text>
|
||||
{formatCurrency(data.summary.total.total_egg_value_rp)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user