mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
376 lines
11 KiB
TypeScript
376 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import React, { useMemo } from 'react';
|
|
import { ColumnDef } from '@tanstack/react-table';
|
|
import Table from '@/components/Table';
|
|
import Card from '@/components/Card';
|
|
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
|
|
import { isResponseSuccess } from '@/lib/api-helper';
|
|
import { BaseSales, ClosingSalesSummary } from '@/types/api/closing';
|
|
import { Product } from '@/types/api/master-data/product';
|
|
import { Customer } from '@/types/api/master-data/customer';
|
|
import { Kandang } from '@/types/api/master-data/kandang';
|
|
import { ClosingApi } from '@/services/api/closing';
|
|
import { useSearchParams } from 'next/navigation';
|
|
import useSWR from 'swr';
|
|
import SalesClosingSkeleton from '@/components/pages/closing/skeleton/SalesClosingSkeleton';
|
|
|
|
interface SalesClosingTableProps {
|
|
projectFlockId: number;
|
|
}
|
|
|
|
const SalesClosingTable = ({ projectFlockId }: SalesClosingTableProps) => {
|
|
const searchParams = useSearchParams();
|
|
const kandangId = searchParams.get('kandangId');
|
|
|
|
const { data: sales, isLoading } = useSWR(
|
|
kandangId
|
|
? `/closing/sales/${projectFlockId}/${kandangId}`
|
|
: `/closing/sales/${projectFlockId}`,
|
|
() =>
|
|
kandangId
|
|
? ClosingApi.getPenjualanByKandang(projectFlockId, Number(kandangId))
|
|
: ClosingApi.getPenjualan(projectFlockId)
|
|
);
|
|
|
|
const salesData: BaseSales[] = useMemo(() => {
|
|
if (isResponseSuccess(sales)) {
|
|
return sales.data.sales || [];
|
|
}
|
|
return [];
|
|
}, [sales]);
|
|
|
|
const summary: ClosingSalesSummary | undefined = useMemo(() => {
|
|
if (isResponseSuccess(sales)) {
|
|
return sales.data.summary;
|
|
}
|
|
return undefined;
|
|
}, [sales]);
|
|
|
|
const totals = useMemo(() => {
|
|
if (salesData.length === 0) {
|
|
return {
|
|
totalQuantity: 0,
|
|
totalWeight: 0,
|
|
avgWeight: 0,
|
|
avgSalesPrice: 0,
|
|
totalSalesPrice: 0,
|
|
avgActualPrice: 0,
|
|
totalActualPrice: 0,
|
|
};
|
|
}
|
|
|
|
const totalQuantity = salesData.reduce(
|
|
(sum, item) => sum + (item.qty || 0),
|
|
0
|
|
);
|
|
const totalWeight = salesData.reduce(
|
|
(sum, item) => sum + (item.weight || 0),
|
|
0
|
|
);
|
|
const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0;
|
|
|
|
const totalSalesPrice = salesData.reduce(
|
|
(sum, item) => sum + (item.total_sales_price || 0),
|
|
0
|
|
);
|
|
|
|
const validSalesPriceItems = salesData.filter(
|
|
(item) => item.sales_price != null && item.sales_price > 0
|
|
);
|
|
const avgSalesPrice =
|
|
validSalesPriceItems.length > 0
|
|
? validSalesPriceItems.reduce(
|
|
(sum, item) => sum + item.sales_price,
|
|
0
|
|
) / validSalesPriceItems.length
|
|
: 0;
|
|
|
|
const totalActualPrice = salesData.reduce(
|
|
(sum, item) => sum + (item.total_actual_price || 0),
|
|
0
|
|
);
|
|
|
|
const validActualPriceItems = salesData.filter(
|
|
(item) => item.actual_price != null && item.actual_price > 0
|
|
);
|
|
const avgActualPrice =
|
|
validActualPriceItems.length > 0
|
|
? validActualPriceItems.reduce(
|
|
(sum, item) => sum + item.actual_price,
|
|
0
|
|
) / validActualPriceItems.length
|
|
: 0;
|
|
|
|
return {
|
|
totalQuantity,
|
|
totalWeight,
|
|
avgWeight,
|
|
avgSalesPrice,
|
|
totalSalesPrice,
|
|
avgActualPrice,
|
|
totalActualPrice,
|
|
};
|
|
}, [salesData]);
|
|
|
|
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') : '-';
|
|
},
|
|
footer: () => (
|
|
<div className='font-semibold text-gray-900'>Total Penjualan</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'age',
|
|
accessorKey: 'age',
|
|
header: 'Umur',
|
|
cell: (props) => {
|
|
const age = props.row.original.age;
|
|
const week = props.row.original.week;
|
|
return age && week ? `${age} hari (${week} minggu)` : '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'do_number',
|
|
accessorKey: 'do_number',
|
|
header: 'No. DO',
|
|
cell: (props) => props.getValue() || '-',
|
|
},
|
|
{
|
|
id: 'product',
|
|
accessorKey: 'product',
|
|
header: 'Produk',
|
|
cell: (props) => {
|
|
const product = props.getValue() as Product;
|
|
return product?.name || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'customer',
|
|
accessorKey: 'customer',
|
|
header: 'Customer',
|
|
cell: (props) => {
|
|
const customer = props.getValue() as Customer;
|
|
return customer?.name || '-';
|
|
},
|
|
},
|
|
{
|
|
id: 'jumlah',
|
|
header: 'Jumlah',
|
|
columns: [
|
|
{
|
|
id: 'qty',
|
|
accessorKey: 'qty',
|
|
header: 'Kuantitas',
|
|
cell: (props) => {
|
|
const value = props.getValue() as number;
|
|
return <div className='text-left'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-left font-semibold text-gray-900'>
|
|
{formatNumber(totals.totalQuantity)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'weight',
|
|
accessorKey: 'weight',
|
|
header: 'Kg',
|
|
cell: (props) => {
|
|
const value = props.getValue() as number;
|
|
return <div className='text-left'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-left font-semibold text-gray-900'>
|
|
{formatNumber(totals.totalWeight)}
|
|
</div>
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'avg_weight',
|
|
accessorKey: 'avg_weight',
|
|
header: 'AVG (Kg)',
|
|
cell: (props) => {
|
|
const value = props.getValue() as number;
|
|
return <div className='text-left'>{formatNumber(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-left font-semibold text-gray-900'>
|
|
{formatNumber(totals.avgWeight)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'sales_price',
|
|
accessorKey: 'sales_price',
|
|
header: 'Harga Sales (Rp)',
|
|
cell: (props) => {
|
|
const value = props.getValue() as number;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{summary
|
|
? formatCurrency(summary.avg_sales_price)
|
|
: formatCurrency(totals.avgSalesPrice)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'total_sales_price',
|
|
accessorKey: 'total_sales_price',
|
|
header: 'Total Sales (Rp)',
|
|
cell: (props) => {
|
|
const value = props.getValue() as number;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{summary
|
|
? formatCurrency(summary.total_sales_price)
|
|
: formatCurrency(totals.totalSalesPrice)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'actual_price',
|
|
accessorKey: 'actual_price',
|
|
header: 'Harga Act (Rp)',
|
|
cell: (props) => {
|
|
const value = props.getValue() as number;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{summary
|
|
? formatCurrency(summary.avg_actual_price)
|
|
: formatCurrency(totals.avgActualPrice)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'total_actual_price',
|
|
accessorKey: 'total_actual_price',
|
|
header: 'Total Act (Rp)',
|
|
cell: (props) => {
|
|
const value = props.getValue() as number;
|
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
|
},
|
|
footer: () => (
|
|
<div className='text-right font-semibold text-gray-900'>
|
|
{summary
|
|
? formatCurrency(summary.total_actual_price)
|
|
: formatCurrency(totals.totalActualPrice)}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: 'kandang',
|
|
accessorKey: 'kandang',
|
|
header: 'Kandang',
|
|
cell: (props) => {
|
|
const kandang = props.getValue() as Kandang;
|
|
return kandang?.name || '-';
|
|
},
|
|
},
|
|
// {
|
|
// 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 'paid':
|
|
// return 'success';
|
|
// case 'tempo':
|
|
// return 'warning';
|
|
// default:
|
|
// return 'neutral';
|
|
// }
|
|
// };
|
|
|
|
// return (
|
|
// <Badge variant='soft' size='sm' color={getStatusColor(status)}>
|
|
// {status || '-'}
|
|
// </Badge>
|
|
// );
|
|
// },
|
|
// },
|
|
],
|
|
[
|
|
summary,
|
|
totals.avgActualPrice,
|
|
totals.avgSalesPrice,
|
|
totals.avgWeight,
|
|
totals.totalActualPrice,
|
|
totals.totalQuantity,
|
|
totals.totalSalesPrice,
|
|
totals.totalWeight,
|
|
]
|
|
);
|
|
|
|
return (
|
|
<div className='w-full pt-3'>
|
|
<Card
|
|
className={{
|
|
wrapper: 'w-full rounded-lg border-none',
|
|
body: 'p-0',
|
|
title: 'px-2 py-1.5 font-normal text-sm bg-primary text-white',
|
|
collapsible: 'rounded-lg',
|
|
}}
|
|
variant='bordered'
|
|
title='Penjualan'
|
|
collapsible
|
|
defaultCollapsed={false}
|
|
>
|
|
{isLoading ? (
|
|
<SalesClosingSkeleton columns={salesColumns} />
|
|
) : salesData.length === 0 ? (
|
|
<SalesClosingSkeleton
|
|
columns={salesColumns}
|
|
iconName='heroicons:chart-bar'
|
|
/>
|
|
) : (
|
|
<Table
|
|
data={salesData}
|
|
columns={salesColumns}
|
|
isLoading={isLoading}
|
|
renderFooter={salesData.length > 0}
|
|
className={{
|
|
containerClassName: 'w-full mb-0!',
|
|
tableWrapperClassName:
|
|
'overflow-x-auto rounded-tr-none rounded-tl-none',
|
|
tableClassName: 'w-full table-auto text-sm',
|
|
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
|
headerColumnClassName:
|
|
'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
|
|
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',
|
|
tableFooterClassName:
|
|
'bg-gray-100 font-semibold border border-gray-200',
|
|
footerRowClassName: 'border-t-2 border-gray-300',
|
|
footerColumnClassName:
|
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
|
}}
|
|
/>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SalesClosingTable;
|