mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-23 06:45:46 +00:00
Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into feat/FE/US-278/TASK-311-adjustment-purchase-and-expense
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
'use client';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import Table 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';
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { Customer } from '@/types/api/master-data/customer';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
|
||||
interface SalesReportTableProps {
|
||||
type?: 'detail';
|
||||
initialValues?: BaseClosingSales;
|
||||
}
|
||||
|
||||
const SalesReportTable = ({
|
||||
type = 'detail',
|
||||
initialValues,
|
||||
}: SalesReportTableProps) => {
|
||||
const salesData: BaseSales[] = useMemo(() => {
|
||||
return initialValues?.sales || [];
|
||||
}, [initialValues]);
|
||||
|
||||
const totals = useMemo(() => {
|
||||
if (salesData.length === 0) {
|
||||
return {
|
||||
totalQuantity: 0,
|
||||
totalWeight: 0,
|
||||
avgWeight: 0,
|
||||
avgPricePartner: 0,
|
||||
totalPartner: 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 validPriceItems = salesData.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 = salesData.reduce(
|
||||
(sum, item) => sum + (item.total_price || 0),
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
totalQuantity,
|
||||
totalWeight,
|
||||
avgWeight,
|
||||
avgPricePartner,
|
||||
totalPartner,
|
||||
};
|
||||
}, [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) => props.getValue() || '-',
|
||||
},
|
||||
{
|
||||
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: 'price_partner',
|
||||
accessorKey: 'price',
|
||||
header: 'Harga Mitra (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'>
|
||||
{formatCurrency(totals.avgPricePartner)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'total_mitra',
|
||||
accessorKey: 'total_price',
|
||||
header: 'Total Mitra (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'>
|
||||
{formatCurrency(totals.totalPartner)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'price_act',
|
||||
accessorKey: 'price',
|
||||
header: 'Harga Act (Rp)',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'total_act',
|
||||
accessorKey: 'total_price',
|
||||
header: 'Total Act (Rp)',
|
||||
cell: (props) => {
|
||||
const value = props.getValue() as number;
|
||||
return <div className='text-right'>{formatCurrency(value)}</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>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full'>
|
||||
<div className='p-4'>
|
||||
<h2 className='text-xl font-semibold mb-4'>Penjualan</h2>
|
||||
<Card
|
||||
className={{
|
||||
wrapper: 'w-full bg-base-100',
|
||||
body: 'p-0',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
data={salesData}
|
||||
columns={salesColumns}
|
||||
renderFooter={salesData.length > 0}
|
||||
className={{
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerColumnClassName:
|
||||
'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
|
||||
bodyRowClassName:
|
||||
'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r 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>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SalesReportTable;
|
||||
@@ -370,7 +370,7 @@ const RecordingTable = () => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
||||
const [isRejectLoading, setIsRejectLoading] = useState(false);
|
||||
const [approvalNotes, setApprovalNotes] = useState('');
|
||||
const [, setApprovalNotes] = useState('');
|
||||
|
||||
const singleDeleteModal = useModal();
|
||||
const approveModal = useModal();
|
||||
|
||||
Reference in New Issue
Block a user