Files
lti-web-client/src/components/pages/closing/sale/SalesReportTable.tsx
T
2025-12-05 17:49:59 +07:00

562 lines
18 KiB
TypeScript

'use client';
import Tabs from '@/components/Tabs';
import React, { useState, useMemo } from 'react';
import { ColumnDef } from '@tanstack/react-table';
import Table, { CustomHeaderRow } from '@/components/Table';
import Card from '@/components/Card';
import Badge from '@/components/Badge';
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing';
interface SalesReportTableProps {
type?: 'detail';
initialValues?: BaseClosingSales;
}
interface HeaderCell {
id: string;
content: React.ReactNode;
colSpan?: number;
rowSpan?: number;
className: string;
field?: string;
}
const generateCustomHeaders = (template: {
groups: Array<{
label: string;
field?: string;
rowSpan?: number;
colSpan?: number;
subLabels?: string[];
}>;
}): CustomHeaderRow[] => {
const mainRow: Array<{
id: string;
content: React.ReactNode;
colSpan?: number;
rowSpan?: number;
className: string;
}> = [];
const subRow: Array<{
id: string;
content: React.ReactNode;
colSpan?: number;
rowSpan?: number;
className: string;
}> = [];
let subColumnIndex = 0;
template.groups.forEach((group) => {
if (group.subLabels) {
const mainCell: HeaderCell = {
id: `${group.field || 'group'}-${subColumnIndex}`,
content: group.label,
colSpan: group.colSpan,
className:
'px-4 py-3 text-xs font-semibold text-gray-700 text-center whitespace-nowrap border border-gray-200',
};
mainRow.push(mainCell);
group.subLabels.forEach((subLabel) => {
const subCell: HeaderCell = {
id: `sub-${subColumnIndex}`,
content: subLabel,
className:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200',
};
if (group.label === 'Jumlah') {
subCell.field = subLabel === 'Kuantitas' ? 'quantity' : 'weight';
}
subRow.push(subCell);
subColumnIndex++;
});
} else {
const mainCell: HeaderCell = {
id: `${group.field}-header`,
content: group.label,
rowSpan: group.rowSpan,
className:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-200',
};
mainCell.field = group.field;
mainRow.push(mainCell);
}
});
const rows: CustomHeaderRow[] = [
{
id: 'main-header',
cells: mainRow,
className: 'bg-gray-50',
},
];
if (subRow.length > 0) {
rows.push({
id: 'sub-header',
cells: subRow,
className: 'bg-gray-50',
});
}
return rows;
};
// TODO: TEMPORARY - Remove this when backend API returns English field names
const mapIndonesianDataToEnglish = (data: BaseClosingSales): BaseSales[] => {
if (!data || !data.penjualan || !Array.isArray(data.penjualan)) {
return [];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return data.penjualan.map((item: any) => ({
id: item.id,
realization_date: item.tanggal_realisasi,
week_age: item.umur_minggu,
age_label: item.umur_label,
delivery_order_number: item.no_do,
product: item.produk,
product_category: item.jenis_produk,
customer: item.customer,
quantity: item.qty,
weight: item.kg,
average: item.avg,
price: item.harga,
total: item.total,
kandang: item.kandang,
kandang_id: item.kandang_id,
payment_status: item.status_pembayaran,
}));
};
// END TODO
const SalesReportTable = ({
type = 'detail',
initialValues,
}: SalesReportTableProps) => {
const [activeTabId, setActiveTabId] = useState<string>('penjualan');
const salesBroilerData: BaseSales[] = useMemo(() => {
if (activeTabId === 'penjualan' && initialValues) {
// TODO: TEMPORARY - Remove this when backend API returns English field names
if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) {
return mapIndonesianDataToEnglish(initialValues);
}
// END TODO
return [];
}
return [];
}, [initialValues, activeTabId]);
const totals = useMemo(() => {
if (salesBroilerData.length === 0) {
return {
totalQuantity: 0,
totalWeight: 0,
avgWeight: 0,
avgPricePartner: 0,
totalPartner: 0,
avgPriceAct: 0,
totalAct: 0,
};
}
const totalQuantity = salesBroilerData.reduce(
(sum, item) => sum + (item.quantity || 0),
0
);
const totalWeight = salesBroilerData.reduce(
(sum, item) => sum + (item.weight || 0),
0
);
const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0;
const validPriceItems = salesBroilerData.filter(
(item) => item.price != null && item.price > 0
);
const avgPricePartner =
validPriceItems.length > 0
? validPriceItems.reduce((sum, item) => sum + item.price, 0) /
validPriceItems.length
: 0;
const totalPartner = salesBroilerData.reduce(
(sum, item) => sum + (item.total || 0),
0
);
const avgPriceAct = avgPricePartner;
const totalAct = totalPartner;
return {
totalQuantity,
totalWeight,
avgWeight,
avgPricePartner,
totalPartner,
avgPriceAct,
totalAct,
};
}, [salesBroilerData]);
const salesColumns: ColumnDef<BaseSales>[] = useMemo(
() => [
{
id: 'realization_date',
accessorKey: 'realization_date',
header: 'Tanggal Realisasi',
cell: (props) => {
const date = props.row.original.realization_date;
return date ? formatDate(date, 'DD MMM YYYY') : '-';
},
},
{
id: 'age_label',
accessorKey: 'age_label',
header: 'Umur',
cell: (props) => props.getValue() || '-',
},
{
id: 'delivery_order_number',
accessorKey: 'delivery_order_number',
header: 'No. DO',
cell: (props) => props.getValue() || '-',
},
{
id: 'product',
accessorKey: 'product',
header: 'Produk',
cell: (props) => props.getValue() || '-',
},
{
id: 'customer',
accessorKey: 'customer',
header: 'Customer',
cell: (props) => props.getValue() || '-',
},
{
id: 'quantity',
accessorKey: 'quantity',
header: 'Kuantitas',
cell: (props) => {
const value = props.getValue() as number;
const isSummary = props.row.id === 'summary';
return (
<div className={isSummary ? 'text-right' : ''}>
{formatNumber(value)}
</div>
);
},
},
{
id: 'weight',
accessorKey: 'weight',
header: 'Kg',
cell: (props) => {
const value = props.getValue() as number;
const isSummary = props.row.id === 'summary';
return (
<div className={isSummary ? 'text-right' : ''}>
{formatNumber(value)}
</div>
);
},
},
{
id: 'average',
accessorKey: 'average',
header: 'AVG (Kg)',
cell: (props) => {
const value = props.getValue() as number;
const isSummary = props.row.id === 'summary';
return (
<div className={isSummary ? 'text-right' : ''}>
{formatNumber(value)}
</div>
);
},
},
{
id: 'price_partner',
accessorKey: 'price',
header: 'Harga Mitra (Rp)',
cell: (props) => {
const value = props.getValue() as number;
const isSummary = props.row.id === 'summary';
return (
<div className={isSummary ? 'text-right' : ''}>
{formatCurrency(value)}
</div>
);
},
},
{
id: 'total_mitra',
accessorKey: 'total',
header: 'Total Mitra (Rp)',
cell: (props) => {
const value = props.getValue() as number;
const isSummary = props.row.id === 'summary';
return (
<div className={isSummary ? 'text-right' : ''}>
{formatCurrency(value)}
</div>
);
},
},
{
id: 'price_act',
accessorKey: 'price',
header: 'Harga Act (Rp)',
cell: (props) => {
const value = props.getValue() as number;
const isSummary = props.row.id === 'summary';
return (
<div className={isSummary ? 'text-right' : ''}>
{formatCurrency(value)}
</div>
);
},
},
{
id: 'total_act',
accessorKey: 'total',
header: 'Total Act (Rp)',
cell: (props) => {
const value = props.getValue() as number;
const isSummary = props.row.id === 'summary';
return (
<div className={isSummary ? 'text-right' : ''}>
{formatCurrency(value)}
</div>
);
},
},
{
id: 'kandang',
accessorKey: 'kandang',
header: 'Kandang',
cell: (props) => props.getValue() || '-',
},
{
id: 'payment_status',
accessorKey: 'payment_status',
header: 'Status Pembayaran',
cell: (props) => {
const status = props.getValue() as string;
const getStatusColor = (status: string) => {
if (!status) return 'neutral';
switch (status.toLowerCase()) {
case 'lunas':
return 'success';
case 'pending':
return 'warning';
case 'belum lunas':
return 'error';
default:
return 'neutral';
}
};
return (
<Badge variant='soft' size='xs' color={getStatusColor(status)}>
{status || '-'}
</Badge>
);
},
},
],
[]
);
const headerTemplate = {
groups: [
{ label: 'Tanggal Realisasi', field: 'realization_date', rowSpan: 2 },
{ label: 'Umur', field: 'age_label', rowSpan: 2 },
{ label: 'No. DO', field: 'delivery_order_number', rowSpan: 2 },
{ label: 'Produk', field: 'product', rowSpan: 2 },
{ label: 'Customer', field: 'customer', rowSpan: 2 },
{
label: 'Jumlah',
colSpan: 2,
subLabels: ['Kuantitas', 'Kg'],
},
{ label: 'AVG (Kg)', field: 'average', rowSpan: 2 },
{ label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 },
{ label: 'Total Mitra (Rp)', field: 'total_mitra', rowSpan: 2 },
{ label: 'Harga Act (Rp)', field: 'price_act', rowSpan: 2 },
{ label: 'Total Act (Rp)', field: 'total_act', rowSpan: 2 },
{ label: 'Kandang', field: 'kandang', rowSpan: 2 },
{ label: 'Status Pembayaran', field: 'payment_status', rowSpan: 2 },
],
};
const salesCustomHeaderRows = useMemo(
() => generateCustomHeaders(headerTemplate),
[]
);
return (
<>
<section className='w-full'>
<Tabs
className='bg-base-100 p-2'
onTabChange={setActiveTabId}
activeTabId={activeTabId}
tabs={[
{
id: 'penjualan',
label: 'Penjualan',
content: (
<div className='p-4'>
<h2 className='text-xl font-semibold mb-4'>Penjualan</h2>
<Card
className={{
wrapper: 'w-full bg-base-100',
}}
>
<Table
data={salesBroilerData}
columns={salesColumns}
renderCustomHeaders={true}
customHeaderRows={salesCustomHeaderRows}
renderFooter={salesBroilerData.length > 0}
footerContent={
<tfoot>
<tr className='bg-gray-100 font-semibold border border-gray-200'>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '>
Total Penjualan
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatNumber(totals.totalQuantity)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatNumber(totals.totalWeight)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatNumber(totals.avgWeight)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatCurrency(totals.avgPricePartner)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatCurrency(totals.totalPartner)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatCurrency(totals.avgPriceAct)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right '>
{formatCurrency(totals.totalAct)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap '></td>
</tr>
</tfoot>
}
className={{
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'hidden',
bodyRowClassName:
'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
}}
/>
</Card>
</div>
),
},
]}
variant='lifted'
/>
</section>
<section className='w-ful'>
<Card
collapsible={true}
defaultCollapsed={true}
className={{ wrapper: 'w-full bg-base-100' }}
title='(Latest) API Response Sample BE for Sales Report Data'
>
<pre>
{JSON.stringify(
{
code: 200,
status: 'success',
message: 'Retrieved sales report successfully',
data: {
project_type: 'GROWING',
flock_id: '1',
period: 10,
sales: [
{
id: 1,
realization_date: '2025-12-05T02:22:17.443165Z',
age: 20,
do_number: 'SO-DO-10001',
product: {
id: 1,
name: 'Laptop Gaming X500',
product_price: 15000000,
selling_price: 16500000.5,
uom: {
id: 1,
name: 'KG',
},
flags: ['Best Seller', 'New Arrival'],
product_category: {
id: 5,
name: 'Elektronik',
code: 'DOC',
},
},
customer: {
id: 12345,
name: 'PT. Solusi Teknologi Nusantara',
type: 'Perusahaan',
account_number: 'ACC1234567890',
balance: 5000000.75,
pic: {
id: 101,
name: 'Budi Santoso',
email: 'budi.santoso@example.com',
role: 'Manajer Akun',
},
},
qty: 6348,
weight: 19142,
avg_weight: 3.02,
price: 26419,
total_price: 505712498,
kandang: {
id: 1,
name: 'cibeber 1',
},
payment_status: 'Paid',
},
],
},
},
null,
2
)}
</pre>
</Card>
</section>
</>
);
};
export default SalesReportTable;