Files
lti-web-client/src/components/pages/closing/sale/SalesReportTable.tsx
T

484 lines
15 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 } from '@/types/api/closing/closing';
interface SalesReportTableProps {
type?: 'detail';
initialValues?: BaseClosingSales;
}
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) {
mainRow.push({
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-300',
});
group.subLabels.forEach((subLabel) => {
subRow.push({
id: `sub-${subColumnIndex}`,
content: subLabel,
className:
'px-4 py-3 text-xs font-semibold text-gray-700 text-left whitespace-nowrap border border-gray-300 border-t-0',
});
subColumnIndex++;
});
} else {
mainRow.push({
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-300',
});
}
});
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
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mapIndonesianDataToEnglish = (data: any): BaseClosingSales[] => {
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,
age_label: item.umur_label,
umur_minggu: item.umur_minggu,
delivery_order_number: item.no_do,
product: item.produk,
jenis_produk: item.jenis_produk,
customer: item.customer,
quantity: item.qty,
weight: item.kg,
average: item.avg,
price: item.harga,
total: item.total,
kandang: item.kandang,
payment_status: item.status_pembayaran,
}));
};
// END TODO
const SalesReportTable = ({
type = 'detail',
initialValues,
}: SalesReportTableProps) => {
const [activeTabId, setActiveTabId] = useState<string>('penjualan');
const salesBroilerData: BaseClosingSales[] = useMemo(() => {
if (activeTabId === 'penjualan' && initialValues) {
// TODO: TEMPORARY - Remove this when backend API returns English field names
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (initialValues.penjualan && Array.isArray(initialValues.penjualan)) {
return mapIndonesianDataToEnglish(initialValues);
}
// END TODO
return [initialValues];
}
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<BaseClosingSales>[] = 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: 'Ekor',
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: ['Ekor', 'Kg'],
},
{ label: 'AVG (Kg)', field: 'average', rowSpan: 2 },
{ label: 'Harga Mitra (Rp)', field: 'price_partner', rowSpan: 2 },
{ label: 'Total Mitra (Rp)', field: 'total_partner', 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 Ayam Besar
</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'>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300 border-t-0'>
Total Penjualan
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300 border-t-0'>
-
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300 border-t-0'>
-
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300 border-t-0'>
-
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300 border-t-0'>
-
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right border border-gray-300 border-t-0'>
{formatNumber(totals.totalQuantity)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right border border-gray-300 border-t-0'>
{formatNumber(totals.totalWeight)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right border border-gray-300 border-t-0'>
{formatNumber(totals.avgWeight)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right border border-gray-300 border-t-0'>
{formatCurrency(totals.avgPricePartner)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right border border-gray-300 border-t-0'>
{formatCurrency(totals.totalPartner)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right border border-gray-300 border-t-0'>
{formatCurrency(totals.avgPriceAct)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap text-right border border-gray-300 border-t-0'>
{formatCurrency(totals.totalAct)}
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300 border-t-0'>
-
</td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300 border-t-0'>
-
</td>
</tr>
</tfoot>
}
className={{
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'hidden',
bodyRowClassName: 'hover:bg-gray-50 transition-colors',
bodyColumnClassName:
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap border border-gray-300',
}}
/>
</Card>
</div>
),
},
]}
variant='lifted'
/>
</section>
</>
);
};
export default SalesReportTable;