feat(FE): Add skeleton components for closing pages

This commit is contained in:
rstubryan
2026-02-19 10:03:25 +07:00
parent 9c953ca382
commit 8fe19feaac
18 changed files with 1262 additions and 613 deletions
@@ -7,6 +7,7 @@ import { HppItem, ProfitLossItem } from '@/types/api/closing';
import { useSearchParams } from 'next/navigation';
import { useMemo } from 'react';
import useSWR from 'swr';
import FinanceClosingSkeleton from '@/components/pages/closing/skeleton/FinanceClosingSkeleton';
const FinanceClosingTable = ({
projectFlockId,
@@ -82,316 +83,336 @@ const FinanceClosingTable = ({
return (
<div className='flex flex-col gap-4'>
<>
<Card
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<div className='grid grid-cols-2 gap-6'>
<div className='flex flex-col gap-1'>
<div>Laba Rugi Brutto</div>
<div className='text-lg font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.gross_profit.amount
)
: '-'}
{isLoading ? (
<FinanceClosingSkeleton />
) : !isResponseSuccess(finance) ? (
<FinanceClosingSkeleton
iconName='heroicons:chart-bar'
title='Data Keuangan Tidak Ditemukan'
subtitle='Tidak ada data keuangan untuk periode ini.'
/>
) : (
<>
<Card
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
<div className='grid grid-cols-2 gap-6'>
<div className='flex flex-col gap-1'>
<div>Laba Rugi Brutto</div>
<div className='text-lg font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.gross_profit.amount
)
: '-'}
</div>
</div>
<div className='flex flex-col gap-1'>
<div>Laba Rugi Netto</div>
<div className='text-lg font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit.amount
)
: '-'}
</div>
</div>
</div>
<div className='flex flex-col gap-1'>
<div>Laba Rugi Netto</div>
<div className='text-lg font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit.amount
)
: '-'}
</div>
</div>
</div>
</Card>
<Card
title='HPP Purchases'
variant='bordered'
collapsible
className={{
wrapper: 'w-full',
}}
>
<div className='mt-6 p-0 mb-0'>
<Table<HppItem>
data={hppTableData}
isLoading={isLoading}
columns={[
{
header: 'No.',
enableSorting: false,
accessorFn: (item, index) => {
if (item.code === 'custom_row') return '-';
const dataRowsBefore = hppTableData
.slice(0, index)
.filter((row) => row.code !== 'custom_row').length;
return dataRowsBefore + 1;
</Card>
<Card
title='HPP Purchases'
variant='bordered'
collapsible
className={{
wrapper: 'w-full',
}}
>
<div className='mt-6 p-0 mb-0'>
<Table<HppItem>
data={hppTableData}
isLoading={isLoading}
columns={[
{
header: 'No.',
enableSorting: false,
accessorFn: (item, index) => {
if (item.code === 'custom_row') return '-';
const dataRowsBefore = hppTableData
.slice(0, index)
.filter((row) => row.code !== 'custom_row').length;
return dataRowsBefore + 1;
},
footer: (props) => {
return 'HPP';
},
},
footer: (props) => {
return 'HPP';
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => formatTitleCase(item.label || '-'),
},
},
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => formatTitleCase(item.label || '-'),
},
{
header: 'Budgeting',
enableSorting: false,
columns: [
{
header: 'Rp/Ekor',
id: 'budgeting_rp_per_bird',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.rp_per_bird || 0),
footer: (props) => {
return props.column.id === 'budgeting_rp_per_bird' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting
?.rp_per_bird || 0
)
: '-';
{
header: 'Budgeting',
enableSorting: false,
columns: [
{
header: 'Rp/Ekor',
id: 'budgeting_rp_per_bird',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.rp_per_bird || 0),
footer: (props) => {
return props.column.id === 'budgeting_rp_per_bird' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting
?.rp_per_bird || 0
)
: '-';
},
},
},
{
header: 'Rp/Kg',
id: 'budgeting_rp_per_kg',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.rp_per_kg || 0),
footer: (props) => {
return props.column.id === 'budgeting_rp_per_kg' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting?.rp_per_kg ||
0
)
: '-';
{
header: 'Rp/Kg',
id: 'budgeting_rp_per_kg',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.rp_per_kg || 0),
footer: (props) => {
return props.column.id === 'budgeting_rp_per_kg' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting
?.rp_per_kg || 0
)
: '-';
},
},
},
{
header: 'Jumlah (Rp)',
id: 'budgeting_amount',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.amount || 0),
footer: (props) => {
return props.column.id === 'budgeting_amount' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting?.amount || 0
)
: '-';
{
header: 'Jumlah (Rp)',
id: 'budgeting_amount',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.budgeting?.amount || 0),
footer: (props) => {
return props.column.id === 'budgeting_amount' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.budgeting?.amount || 0
)
: '-';
},
},
},
],
},
{
header: 'Realization',
enableSorting: false,
columns: [
{
header: 'Rp/Ekor',
id: 'realization_rp_per_bird',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.rp_per_bird || 0),
footer: (props) => {
return props.column.id === 'realization_rp_per_bird' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization
?.rp_per_bird || 0
)
: '-';
],
},
{
header: 'Realization',
enableSorting: false,
columns: [
{
header: 'Rp/Ekor',
id: 'realization_rp_per_bird',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.rp_per_bird || 0),
footer: (props) => {
return props.column.id ===
'realization_rp_per_bird' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization
?.rp_per_bird || 0
)
: '-';
},
},
},
{
header: 'Rp/Kg',
id: 'realization_rp_per_kg',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.rp_per_kg || 0),
footer: (props) => {
return props.column.id === 'realization_rp_per_kg' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization
?.rp_per_kg || 0
)
: '-';
{
header: 'Rp/Kg',
id: 'realization_rp_per_kg',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.rp_per_kg || 0),
footer: (props) => {
return props.column.id === 'realization_rp_per_kg' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization
?.rp_per_kg || 0
)
: '-';
},
},
},
{
header: 'Jumlah (Rp)',
id: 'realization_amount',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.amount || 0),
footer: (props) => {
return props.column.id === 'realization_amount' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization?.amount || 0
)
: '-';
{
header: 'Jumlah (Rp)',
id: 'realization_amount',
enableSorting: false,
accessorFn: (item) =>
formatCurrency(item.realization?.amount || 0),
footer: (props) => {
return props.column.id === 'realization_amount' &&
isResponseSuccess(finance)
? formatCurrency(
finance.data.hpp.summary?.realization?.amount ||
0
)
: '-';
},
},
},
],
},
]}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.code === 'custom_row') {
return (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
></td>
<td
colSpan={7}
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
],
},
]}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.code === 'custom_row') {
return (
<tr
key={row.id}
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
>
<div className='font-bold'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
</tr>
);
}
return null;
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
<Card
title='Profit/Loss'
variant='bordered'
collapsible
className={{
wrapper: 'w-full',
}}
>
<div className='mt-6 p-0 mb-0'>
<Table<ProfitLossItem>
data={profitLossTableData}
isLoading={isLoading}
columns={[
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => item.label,
cell: (item) => (
<div className=''>
{formatTitleCase(item.row.original.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: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_bird || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Rp/Kg',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_kg || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Jumlah (Rp)',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.amount || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.amount || 0
)
: formatCurrency(0)}
</div>
),
},
]}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.code === 'custom_row') {
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 null;
}}
className={{
paginationClassName: 'hidden',
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
</>
<td
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
></td>
<td
colSpan={7}
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
>
<div className='font-bold'>
{formatTitleCase(rowData.label ?? '-')}
</div>
</td>
</tr>
);
}
return null;
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
<Card
title='Profit/Loss'
variant='bordered'
collapsible
className={{
wrapper: 'w-full',
}}
>
<div className='mt-6 p-0 mb-0'>
<Table<ProfitLossItem>
data={profitLossTableData}
isLoading={isLoading}
columns={[
{
header: 'Jenis',
enableSorting: false,
accessorFn: (item) => item.label,
cell: (item) => (
<div className=''>
{formatTitleCase(item.row.original.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: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_bird || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Rp/Kg',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.rp_per_kg || 0
)
: formatCurrency(0)}
</div>
),
},
{
header: 'Jumlah (Rp)',
enableSorting: false,
accessorFn: (item) => formatCurrency(item.amount || 0),
footer: () => (
<div className='font-bold'>
{isResponseSuccess(finance)
? formatCurrency(
finance.data.profit_loss.summary.net_profit
.amount || 0
)
: formatCurrency(0)}
</div>
),
},
]}
renderCustomRow={(row) => {
const rowData = row.original;
if (rowData.code === 'custom_row') {
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 null;
}}
className={{
paginationClassName: 'hidden',
}}
renderFooter={isResponseSuccess(finance)}
/>
</div>
</Card>
</>
)}
</div>
);
};