feat(FE): adding closing finance per kandang

This commit is contained in:
randy-ar
2026-01-15 20:08:21 +07:00
parent 5a4e3ab5ab
commit 8c6a87c011
2 changed files with 139 additions and 359 deletions
@@ -3,54 +3,11 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { formatCurrency, formatTitleCase } from '@/lib/helper'; import { formatCurrency, formatTitleCase } from '@/lib/helper';
import { ClosingApi } from '@/services/api/closing'; import { ClosingApi } from '@/services/api/closing';
import { import { HppItem, ProfitLossItem } from '@/types/api/closing';
DataSummarySubTotal,
HppPurchaseData,
ProfitLossDataAmount,
} from '@/types/api/closing';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
type HppTableRow =
| (HppPurchaseData & {
group_name: string;
group_index: number;
isGroupHeader?: boolean;
})
| {
group_name: string;
group_index: number;
isGroupHeader: true;
type?: never;
budgeting?: never;
realization?: never;
}
| {
type: string;
group_name: string;
group_index: number;
isGroupHeader: false;
budgeting?: { rp_per_bird: number; rp_per_kg: number; amount: number };
realization?: { rp_per_bird: number; rp_per_kg: number; amount: number };
};
type ProfitLossTableRow =
| (DataSummarySubTotal & {
type: string;
group_name: string;
group_index: number;
isGroupHeader?: boolean;
})
| {
group_name: string;
group_index: number;
isGroupHeader: true;
type?: never;
rp_per_bird?: never;
rp_per_kg?: never;
amount?: never;
};
const ClosingFinanceTable = ({ const ClosingFinanceTable = ({
projectFlockId, projectFlockId,
}: { }: {
@@ -68,167 +25,60 @@ const ClosingFinanceTable = ({
) )
); );
const staticHppRows: Array<{ const hppTableData: HppItem[] = useMemo(() => {
group_name: string; if (isResponseSuccess(finance)) {
type: string; const customItems = {
group_index: number; label: 'HPP dan Pengeluaran',
}> = [ code: 'custom_row',
{ } as HppItem;
group_name: 'HPP dan Pengeluaran', const purchases = finance.data.hpp.items.filter(
type: 'Pembelian PAKAN', (item) => item.category === 'purchase'
group_index: 0, );
}, const totalBudgeting = {
{ label: 'HPP dan Bahan Baku',
group_name: 'HPP dan Pengeluaran', code: 'custom_row',
type: 'Pembelian STARTER', } as HppItem;
group_index: 0, const overheads = finance.data.hpp.items.filter(
}, (item) => item.category === 'overhead'
{ );
group_name: 'HPP dan Pengeluaran', return [customItems, ...purchases, totalBudgeting, ...overheads];
type: 'Pembelian DOC', }
group_index: 0, return [];
}, }, []);
{
group_name: 'HPP dan Pengeluaran',
type: 'Pembelian PULLET',
group_index: 0,
},
{
group_name: 'HPP dan Pengeluaran',
type: 'Pembelian LAYER',
group_index: 0,
},
{
group_name: 'HPP dan Bahan Baku',
type: 'Pengeluaran Overhead',
group_index: 1,
},
{
group_name: 'HPP dan Bahan Baku',
type: 'Beban Ekspedisi',
group_index: 1,
},
];
const hppTableData: HppTableRow[] = [ const profitLossTableData: ProfitLossItem[] = useMemo(() => {
{ if (isResponseSuccess(finance)) {
group_name: 'HPP dan Pengeluaran', const incomes = finance.data.profit_loss.items.filter(
group_index: 0, (item) => item.type === 'income'
isGroupHeader: true as const, );
}, const purchases = finance.data.profit_loss.items.filter(
...staticHppRows (item) => item.type === 'purchase'
.filter((row) => row.group_index === 0) );
.map((staticRow) => { const overheads = finance.data.profit_loss.items.filter(
const apiData = isResponseSuccess(finance) (item) => item.type === 'overhead'
? finance.data.hpp_purchases.hpp );
.find((g) => g.group_name === staticRow.group_name) const grossProfit = {
?.data.find((d) => d.type === staticRow.type) label: 'LABA RUGI BRUTO',
: null; code: 'custom_row',
type: 'gross_profit',
return { rp_per_bird:
group_name: staticRow.group_name, finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
group_index: staticRow.group_index, rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
type: staticRow.type, amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
budgeting: apiData?.budgeting || { } as ProfitLossItem;
rp_per_bird: 0, const subtotal = {
rp_per_kg: 0, label: 'Subtotal',
amount: 0, code: 'custom_row',
}, type: 'subtotal',
realization: apiData?.realization || { rp_per_bird:
rp_per_bird: 0, finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
rp_per_kg: 0, rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
amount: 0, amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
}, } as ProfitLossItem;
isGroupHeader: false as const, return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
}; }
}), return [];
{ }, []);
group_name: 'HPP dan Bahan Baku',
group_index: 1,
isGroupHeader: true as const,
},
...staticHppRows
.filter((row) => row.group_index === 1)
.map((staticRow) => {
const apiData = isResponseSuccess(finance)
? finance.data.hpp_purchases.hpp
.find((g) => g.group_name === staticRow.group_name)
?.data.find((d) => d.type === staticRow.type)
: null;
return {
group_name: staticRow.group_name,
group_index: staticRow.group_index,
type: staticRow.type,
budgeting: apiData?.budgeting || {
rp_per_bird: 0,
rp_per_kg: 0,
amount: 0,
},
realization: apiData?.realization || {
rp_per_bird: 0,
rp_per_kg: 0,
amount: 0,
},
isGroupHeader: false as const,
};
}),
{
group_name: 'HPP',
group_index: 2,
isGroupHeader: true as const,
},
];
const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance)
? [
// Pembelian group
...finance.data.profit_loss.data.pembelian.map((item) => ({
label: 'Pembelian',
group_name: 'Pembelian',
group_index: 1,
type: item.type,
rp_per_bird: item.rp_per_bird,
rp_per_kg: item.rp_per_kg,
amount: item.amount,
isGroupHeader: false as const,
})),
{
label: finance.data.profit_loss.data.summary.gross_profit.label,
group_name: 'Penjualan',
group_index: 0,
isGroupHeader: true as const,
type: finance.data.profit_loss.data.summary.gross_profit.label,
rp_per_bird:
finance.data.profit_loss.data.summary.gross_profit.rp_per_bird,
rp_per_kg:
finance.data.profit_loss.data.summary.gross_profit.rp_per_kg,
amount: finance.data.profit_loss.data.summary.gross_profit.amount,
},
// Penjualan group
...finance.data.profit_loss.data.penjualan.map((item) => ({
label: 'Penjualan',
group_name: 'Penjualan',
group_index: 0,
type: item.type,
rp_per_bird: item.rp_per_bird,
rp_per_kg: item.rp_per_kg,
amount: item.amount,
isGroupHeader: false as const,
})),
{
label: finance.data.profit_loss.data.summary.sub_total.label,
group_name: 'Pembelian',
group_index: 1,
isGroupHeader: true as const,
type: finance.data.profit_loss.data.summary.sub_total.label,
rp_per_bird:
finance.data.profit_loss.data.summary.sub_total.rp_per_bird,
rp_per_kg: finance.data.profit_loss.data.summary.sub_total.rp_per_kg,
amount: finance.data.profit_loss.data.summary.sub_total.amount,
},
]
: [];
return ( return (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
@@ -241,47 +91,30 @@ const ClosingFinanceTable = ({
> >
<div className='grid grid-cols-2 gap-6'> <div className='grid grid-cols-2 gap-6'>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<div> <div>Laba Rugi Brutto</div>
{isResponseSuccess(finance)
? formatTitleCase(
finance.data.profit_loss.data.summary.gross_profit
.label || '-'
)
: 'Laba Rugi Brutto'}
</div>
<div className='text-lg font-bold'> <div className='text-lg font-bold'>
{isResponseSuccess(finance) {isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.profit_loss.data.summary.gross_profit.amount finance.data.profit_loss.summary.gross_profit.amount
) )
: '-'} : '-'}
</div> </div>
</div> </div>
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<div> <div>Laba Rugi Netto</div>
{isResponseSuccess(finance)
? formatTitleCase(
finance.data.profit_loss.data.summary.net_profit.label ||
'-'
)
: 'Laba Rugi Netto'}
</div>
<div className='text-lg font-bold'> <div className='text-lg font-bold'>
{isResponseSuccess(finance) {isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.profit_loss.data.summary.net_profit.amount finance.data.profit_loss.summary.net_profit.amount
) )
: '-'} : '-'}
</div> </div>
</div> </div>
</div> </div>
</Card> </Card>
{JSON.stringify(finance)}
<Card <Card
title={ title='HPP Purchases'
isResponseSuccess(finance)
? finance.data.hpp_purchases.title
: 'HPP Purchases'
}
variant='bordered' variant='bordered'
collapsible collapsible
className={{ className={{
@@ -289,7 +122,7 @@ const ClosingFinanceTable = ({
}} }}
> >
<div className='mt-6 p-0 mb-0'> <div className='mt-6 p-0 mb-0'>
<Table<HppTableRow> <Table<HppItem>
data={hppTableData} data={hppTableData}
isLoading={isLoading} isLoading={isLoading}
columns={[ columns={[
@@ -297,10 +130,10 @@ const ClosingFinanceTable = ({
header: 'No.', header: 'No.',
enableSorting: false, enableSorting: false,
accessorFn: (item, index) => { accessorFn: (item, index) => {
if (item.isGroupHeader) return '-'; if (item.code === 'custom_row') return '-';
const dataRowsBefore = hppTableData const dataRowsBefore = hppTableData
.slice(0, index) .slice(0, index)
.filter((row) => !row.isGroupHeader).length; .filter((row) => row.code !== 'custom_row').length;
return dataRowsBefore + 1; return dataRowsBefore + 1;
}, },
footer: (props) => { footer: (props) => {
@@ -310,7 +143,7 @@ const ClosingFinanceTable = ({
{ {
header: 'Jenis', header: 'Jenis',
enableSorting: false, enableSorting: false,
accessorFn: (item) => formatTitleCase(item.type || '-'), accessorFn: (item) => formatTitleCase(item.label || '-'),
}, },
{ {
header: 'Budgeting', header: 'Budgeting',
@@ -326,7 +159,7 @@ const ClosingFinanceTable = ({
return props.column.id === 'budgeting_rp_per_bird' && return props.column.id === 'budgeting_rp_per_bird' &&
isResponseSuccess(finance) isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.hpp_purchases.summary_hpp?.budgeting finance.data.hpp.summary?.budgeting
?.rp_per_bird || 0 ?.rp_per_bird || 0
) )
: '-'; : '-';
@@ -342,8 +175,8 @@ const ClosingFinanceTable = ({
return props.column.id === 'budgeting_rp_per_kg' && return props.column.id === 'budgeting_rp_per_kg' &&
isResponseSuccess(finance) isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.hpp_purchases.summary_hpp?.budgeting finance.data.hpp.summary?.budgeting?.rp_per_kg ||
?.rp_per_kg || 0 0
) )
: '-'; : '-';
}, },
@@ -358,8 +191,7 @@ const ClosingFinanceTable = ({
return props.column.id === 'budgeting_amount' && return props.column.id === 'budgeting_amount' &&
isResponseSuccess(finance) isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.hpp_purchases.summary_hpp?.budgeting finance.data.hpp.summary?.budgeting?.amount || 0
?.amount || 0
) )
: '-'; : '-';
}, },
@@ -380,8 +212,8 @@ const ClosingFinanceTable = ({
return props.column.id === 'realization_rp_per_bird' && return props.column.id === 'realization_rp_per_bird' &&
isResponseSuccess(finance) isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.hpp_purchases.summary_hpp finance.data.hpp.summary?.realization
?.realization?.rp_per_bird || 0 ?.rp_per_bird || 0
) )
: '-'; : '-';
}, },
@@ -396,8 +228,8 @@ const ClosingFinanceTable = ({
return props.column.id === 'realization_rp_per_kg' && return props.column.id === 'realization_rp_per_kg' &&
isResponseSuccess(finance) isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.hpp_purchases.summary_hpp finance.data.hpp.summary?.realization
?.realization?.rp_per_kg || 0 ?.rp_per_kg || 0
) )
: '-'; : '-';
}, },
@@ -412,8 +244,7 @@ const ClosingFinanceTable = ({
return props.column.id === 'realization_amount' && return props.column.id === 'realization_amount' &&
isResponseSuccess(finance) isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.hpp_purchases.summary_hpp finance.data.hpp.summary?.realization?.amount || 0
?.realization?.amount || 0
) )
: '-'; : '-';
}, },
@@ -423,7 +254,7 @@ const ClosingFinanceTable = ({
]} ]}
renderCustomRow={(row) => { renderCustomRow={(row) => {
const rowData = row.original; const rowData = row.original;
if (rowData.isGroupHeader) { if (rowData.code === 'custom_row') {
return ( return (
<tr <tr
key={row.id} key={row.id}
@@ -437,7 +268,7 @@ const ClosingFinanceTable = ({
className={TABLE_DEFAULT_STYLING.bodyColumnClassName} className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
> >
<div className='font-bold'> <div className='font-bold'>
{formatTitleCase(rowData.group_name ?? '-')} {formatTitleCase(rowData.label ?? '-')}
</div> </div>
</td> </td>
</tr> </tr>
@@ -450,11 +281,7 @@ const ClosingFinanceTable = ({
</div> </div>
</Card> </Card>
<Card <Card
title={ title='Profit/Loss'
isResponseSuccess(finance)
? finance.data.profit_loss.title
: 'Profit/Loss'
}
variant='bordered' variant='bordered'
collapsible collapsible
className={{ className={{
@@ -462,39 +289,32 @@ const ClosingFinanceTable = ({
}} }}
> >
<div className='mt-6 p-0 mb-0'> <div className='mt-6 p-0 mb-0'>
<Table<ProfitLossTableRow> <Table<ProfitLossItem>
data={profitLossTableData} data={profitLossTableData}
isLoading={isLoading} isLoading={isLoading}
columns={[ columns={[
{ {
header: 'Jenis', header: 'Jenis',
enableSorting: false, enableSorting: false,
accessorFn: (item) => item.type, accessorFn: (item) => item.label,
cell: (item) => ( cell: (item) => (
<div className=''> <div className=''>
{formatTitleCase(item.row.original.type || '-')} {formatTitleCase(item.row.original.label || '-')}
</div> </div>
), ),
footer: (item) => ( footer: () => (
<div className='font-bold uppercase'> <div className='font-bold uppercase'>LABA RUGI NETTO</div>
{isResponseSuccess(finance)
? formatTitleCase(
finance.data.profit_loss.data.summary.net_profit
.label || '-'
)
: '-'}
</div>
), ),
}, },
{ {
header: 'Rp/Ekor', header: 'Rp/Ekor',
enableSorting: false, enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0), accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
footer: (item) => ( footer: () => (
<div className='font-bold'> <div className='font-bold'>
{isResponseSuccess(finance) {isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.profit_loss.data.summary.net_profit finance.data.profit_loss.summary.net_profit
.rp_per_bird || 0 .rp_per_bird || 0
) )
: formatCurrency(0)} : formatCurrency(0)}
@@ -505,11 +325,11 @@ const ClosingFinanceTable = ({
header: 'Rp/Kg', header: 'Rp/Kg',
enableSorting: false, enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0), accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
footer: (item) => ( footer: () => (
<div className='font-bold'> <div className='font-bold'>
{isResponseSuccess(finance) {isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.profit_loss.data.summary.net_profit finance.data.profit_loss.summary.net_profit
.rp_per_kg || 0 .rp_per_kg || 0
) )
: formatCurrency(0)} : formatCurrency(0)}
@@ -520,11 +340,11 @@ const ClosingFinanceTable = ({
header: 'Jumlah (Rp)', header: 'Jumlah (Rp)',
enableSorting: false, enableSorting: false,
accessorFn: (item) => formatCurrency(item.amount || 0), accessorFn: (item) => formatCurrency(item.amount || 0),
footer: (item) => ( footer: () => (
<div className='font-bold'> <div className='font-bold'>
{isResponseSuccess(finance) {isResponseSuccess(finance)
? formatCurrency( ? formatCurrency(
finance.data.profit_loss.data.summary.net_profit finance.data.profit_loss.summary.net_profit
.amount || 0 .amount || 0
) )
: formatCurrency(0)} : formatCurrency(0)}
@@ -534,55 +354,30 @@ const ClosingFinanceTable = ({
]} ]}
renderCustomRow={(row) => { renderCustomRow={(row) => {
const rowData = row.original; const rowData = row.original;
if (rowData.isGroupHeader) { if (rowData.code === 'custom_row') {
if (rowData.amount) {
return (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.footerRowClassName}
>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold ps-6 uppercase'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_bird ?? 0)}
</div>
</td>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_kg ?? 0)}
</div>
</td>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatCurrency(rowData.amount ?? 0)}
</div>
</td>
</tr>
);
}
return ( return (
<tr <tr
key={row.id} key={row.id}
className={TABLE_DEFAULT_STYLING.bodyRowClassName} className={TABLE_DEFAULT_STYLING.footerRowClassName}
> >
<td <td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
colSpan={4} <div className='font-bold ps-6 uppercase'>
className={TABLE_DEFAULT_STYLING.bodyColumnClassName} {formatTitleCase(rowData.label ?? '-')}
> </div>
</td>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold'> <div className='font-bold'>
{formatTitleCase(rowData.group_name ?? '-')} {formatCurrency(rowData.rp_per_bird ?? 0)}
</div>
</td>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold'>
{formatCurrency(rowData.rp_per_kg ?? 0)}
</div>
</td>
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
<div className='font-bold'>
{formatCurrency(rowData.amount ?? 0)}
</div> </div>
</td> </td>
</tr> </tr>
+32 -47
View File
@@ -219,64 +219,30 @@ export type ClosingSales = BaseMetadata & BaseClosingSales;
// ====== FINANCE ====== // ====== FINANCE ======
export interface ClosingFinance { export interface ClosingFinance {
project_flock_id: number; hpp: ClosingFinanceHpp;
period: number;
project_type: string;
volume_base: ClosingFinanceVolumeBase;
hpp_purchases: ClosingFinanceHppPurchases;
profit_loss: ClosingFinanceProfitLoss; profit_loss: ClosingFinanceProfitLoss;
} }
export interface ClosingFinanceProfitLoss { export interface ClosingFinanceHpp {
title: string; items: HppItem[];
data: ProfitLossData; summary: HppSummary;
} }
export interface ClosingFinanceHppPurchases { export interface HppItem {
title: string; id: number;
hpp: GroupHppPurchase[]; category: string;
summary_hpp: HppPurchasesSummary; code: string;
}
export interface ClosingFinanceVolumeBase {
total_birds: number;
total_weight_kg: number;
}
export interface ProfitLossData {
penjualan: ProfitLossDataAmount[];
pembelian: ProfitLossDataAmount[];
summary: ProfitLossDataSummary;
}
export interface GroupHppPurchase {
group_name: string;
data: HppPurchaseData[];
}
export interface ProfitLossDataSummary {
gross_profit: DataSummarySubTotal;
sub_total: DataSummarySubTotal;
net_profit: DataSummarySubTotal;
}
export interface ProfitLossDataAmount {
type: string;
rp_per_bird: number;
rp_per_kg: number;
amount: number;
}
export interface HppPurchasesSummary {
label: string; label: string;
budgeting: HppPurchaseDataAmount; budgeting: HppPurchaseDataAmount;
realization: HppPurchaseDataAmount; realization: HppPurchaseDataAmount;
} }
export interface HppPurchaseData { export interface HppSummary {
type: string; label: string;
budgeting: HppPurchaseDataAmount; budgeting: HppPurchaseDataAmount;
realization: HppPurchaseDataAmount; realization: HppPurchaseDataAmount;
egg_budgeting: HppPurchaseDataAmount;
egg_realization: HppPurchaseDataAmount;
} }
export interface HppPurchaseDataAmount { export interface HppPurchaseDataAmount {
@@ -285,8 +251,27 @@ export interface HppPurchaseDataAmount {
amount: number; amount: number;
} }
export interface DataSummarySubTotal { export interface ClosingFinanceProfitLoss {
items: ProfitLossItem[];
summary: ProfitLossSummary;
}
export interface ProfitLossItem {
code: string;
label: string; label: string;
type: string;
rp_per_bird: number;
rp_per_kg: number;
amount: number;
}
export interface ProfitLossSummary {
gross_profit: ProfitLossAmount;
sub_total: ProfitLossAmount;
net_profit: ProfitLossAmount;
}
export interface ProfitLossAmount {
rp_per_bird: number; rp_per_bird: number;
rp_per_kg: number; rp_per_kg: number;
amount: number; amount: number;