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 { 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,
}: {
@@ -68,167 +25,60 @@ const ClosingFinanceTable = ({
)
);
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 [];
}, []);
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,
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,
},
]
: [];
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.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.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 [];
}, []);
return (
<div className='flex flex-col gap-4'>
@@ -241,47 +91,30 @@ 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>
</div>
</div>
</Card>
{JSON.stringify(finance)}
<Card
title={
isResponseSuccess(finance)
? finance.data.hpp_purchases.title
: 'HPP Purchases'
}
title='HPP Purchases'
variant='bordered'
collapsible
className={{
@@ -289,7 +122,7 @@ const ClosingFinanceTable = ({
}}
>
<div className='mt-6 p-0 mb-0'>
<Table<HppTableRow>
<Table<HppItem>
data={hppTableData}
isLoading={isLoading}
columns={[
@@ -297,10 +130,10 @@ const ClosingFinanceTable = ({
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) => {
@@ -310,7 +143,7 @@ const ClosingFinanceTable = ({
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => formatTitleCase(item.type || '-'),
accessorFn: (item) => formatTitleCase(item.label || '-'),
},
{
header: 'Budgeting',
@@ -326,7 +159,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
)
: '-';
@@ -342,8 +175,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
)
: '-';
},
@@ -358,8 +191,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
)
: '-';
},
@@ -380,8 +212,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
)
: '-';
},
@@ -396,8 +228,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
)
: '-';
},
@@ -412,8 +244,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
)
: '-';
},
@@ -423,7 +254,7 @@ const ClosingFinanceTable = ({
]}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.isGroupHeader) {
if (rowData.code === 'custom_row') {
return (
<tr
key={row.id}
@@ -437,7 +268,7 @@ const ClosingFinanceTable = ({
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatTitleCase(rowData.group_name ?? '-')}
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
</tr>
@@ -450,11 +281,7 @@ const ClosingFinanceTable = ({
</div>
</Card>
<Card
title={
isResponseSuccess(finance)
? finance.data.profit_loss.title
: 'Profit/Loss'
}
title='Profit/Loss'
variant='bordered'
collapsible
className={{
@@ -462,39 +289,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)}
@@ -505,11 +325,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)}
@@ -520,11 +340,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)}
@@ -534,55 +354,30 @@ const ClosingFinanceTable = ({
]}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.isGroupHeader) {
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>
);
}
if (rowData.code === 'custom_row') {
return (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
className={TABLE_DEFAULT_STYLING.footerRowClassName}
>
<td
colSpan={4}
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}>
<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>
</td>
</tr>
+32 -47
View File
@@ -219,64 +219,30 @@ export type ClosingSales = BaseMetadata & BaseClosingSales;
// ====== FINANCE ======
export interface ClosingFinance {
project_flock_id: number;
period: number;
project_type: string;
volume_base: ClosingFinanceVolumeBase;
hpp_purchases: ClosingFinanceHppPurchases;
hpp: ClosingFinanceHpp;
profit_loss: ClosingFinanceProfitLoss;
}
export interface ClosingFinanceProfitLoss {
title: string;
data: ProfitLossData;
export interface ClosingFinanceHpp {
items: HppItem[];
summary: HppSummary;
}
export interface ClosingFinanceHppPurchases {
title: string;
hpp: GroupHppPurchase[];
summary_hpp: HppPurchasesSummary;
}
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 {
export interface HppItem {
id: number;
category: string;
code: string;
label: string;
budgeting: HppPurchaseDataAmount;
realization: HppPurchaseDataAmount;
}
export interface HppPurchaseData {
type: string;
export interface HppSummary {
label: string;
budgeting: HppPurchaseDataAmount;
realization: HppPurchaseDataAmount;
egg_budgeting: HppPurchaseDataAmount;
egg_realization: HppPurchaseDataAmount;
}
export interface HppPurchaseDataAmount {
@@ -285,8 +251,27 @@ export interface HppPurchaseDataAmount {
amount: number;
}
export interface DataSummarySubTotal {
export interface ClosingFinanceProfitLoss {
items: ProfitLossItem[];
summary: ProfitLossSummary;
}
export interface ProfitLossItem {
code: 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_kg: number;
amount: number;