Sales Per Product Report
This commit is contained in:
parent
07c0bdb3af
commit
073f3dd89c
@ -0,0 +1,18 @@
|
||||
import ReportTitle from '@/components/report/ReportTitle'
|
||||
import ReportSalesPerProductContent from '@/views/apps/report/sales/sales-per-product/ReportSalesPerProductContent'
|
||||
import Grid from '@mui/material/Grid2'
|
||||
|
||||
const SalesProductReportPage = () => {
|
||||
return (
|
||||
<Grid container spacing={6}>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<ReportTitle title='Laporan Penjualan per Produk' />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<ReportSalesPerProductContent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default SalesProductReportPage
|
||||
@ -16,36 +16,36 @@ const ReportSalesList: React.FC = () => {
|
||||
iconClass: 'tabler-receipt-2',
|
||||
link: getLocalizedUrl(`/apps/report/sales/sales-report`, locale as Locale)
|
||||
},
|
||||
{
|
||||
title: 'Detail Penjualan',
|
||||
iconClass: 'tabler-receipt-2',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
title: 'Tagihan Pelanggan',
|
||||
iconClass: 'tabler-receipt-2',
|
||||
link: ''
|
||||
},
|
||||
// {
|
||||
// title: 'Detail Penjualan',
|
||||
// iconClass: 'tabler-receipt-2',
|
||||
// link: ''
|
||||
// },
|
||||
// {
|
||||
// title: 'Tagihan Pelanggan',
|
||||
// iconClass: 'tabler-receipt-2',
|
||||
// link: ''
|
||||
// },
|
||||
{
|
||||
title: 'Penjualan per Produk',
|
||||
iconClass: 'tabler-receipt-2',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
title: 'Penjualan per Kategori Produk',
|
||||
iconClass: 'tabler-receipt-2',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
title: 'Penjualan Produk per Pelanggan',
|
||||
iconClass: 'tabler-receipt-2',
|
||||
link: ''
|
||||
},
|
||||
{
|
||||
title: 'Pemesanan per Produk',
|
||||
iconClass: 'tabler-receipt-2',
|
||||
link: ''
|
||||
link: getLocalizedUrl(`/apps/report/sales/sales-product`, locale as Locale)
|
||||
}
|
||||
// {
|
||||
// title: 'Penjualan per Kategori Produk',
|
||||
// iconClass: 'tabler-receipt-2',
|
||||
// link: ''
|
||||
// },
|
||||
// {
|
||||
// title: 'Penjualan Produk per Pelanggan',
|
||||
// iconClass: 'tabler-receipt-2',
|
||||
// link: ''
|
||||
// },
|
||||
// {
|
||||
// title: 'Pemesanan per Produk',
|
||||
// iconClass: 'tabler-receipt-2',
|
||||
// link: ''
|
||||
// }
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@ -0,0 +1,207 @@
|
||||
'use client'
|
||||
|
||||
import DateRangePicker from '@/components/RangeDatePicker'
|
||||
import { ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
|
||||
import { useProductSalesAnalytics } from '@/services/queries/analytics'
|
||||
import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform'
|
||||
import { Button, Card, CardContent } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
|
||||
const ReportSalesPerProductContent = () => {
|
||||
const [startDate, setStartDate] = useState<Date | null>(new Date())
|
||||
const [endDate, setEndDate] = useState<Date | null>(new Date())
|
||||
|
||||
const { data: products } = useProductSalesAnalytics({
|
||||
date_from: formatDateDDMMYYYY(startDate!),
|
||||
date_to: formatDateDDMMYYYY(endDate!)
|
||||
})
|
||||
|
||||
const productSummary = {
|
||||
totalQuantitySold: products?.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0,
|
||||
totalRevenue: products?.data?.reduce((sum, item) => sum + (item?.revenue || 0), 0) || 0,
|
||||
totalOrders: products?.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className='p-6 border-be'>
|
||||
<div className='flex items-center justify-end gap-2'>
|
||||
<Button
|
||||
color='secondary'
|
||||
variant='tonal'
|
||||
startIcon={<i className='tabler-upload' />}
|
||||
className='max-sm:is-full'
|
||||
// onClick={handleExportPDF}
|
||||
>
|
||||
Ekspor
|
||||
</Button>
|
||||
<DateRangePicker
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent>
|
||||
<ReportItemHeader
|
||||
title='Ringkasan Item'
|
||||
date={`${products?.date_from.split('T')[0]} - ${products?.date_to.split('T')[0]}`}
|
||||
/>
|
||||
<div className='bg-gray-50 border border-gray-200 overflow-visible'>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='w-full table-fixed' style={{ minWidth: '100%' }}>
|
||||
<colgroup>
|
||||
<col style={{ width: '40%' }} />
|
||||
<col style={{ width: '15%' }} />
|
||||
<col style={{ width: '15%' }} />
|
||||
<col style={{ width: '15%' }} />
|
||||
<col style={{ width: '15%' }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr className='text-gray-800 border-b-2 border-gray-300'>
|
||||
<th className='text-left p-3 font-semibold border-r border-gray-300'>Produk</th>
|
||||
<th className='text-center p-3 font-semibold border-r border-gray-300'>Qty</th>
|
||||
<th className='text-center p-3 font-semibold border-r border-gray-300'>Order</th>
|
||||
<th className='text-right p-3 font-semibold border-r border-gray-300'>Pendapatan</th>
|
||||
<th className='text-right p-3 font-semibold'>Rata Rata</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(() => {
|
||||
// Group products by category
|
||||
const groupedProducts =
|
||||
products?.data?.reduce(
|
||||
(acc, item) => {
|
||||
const categoryName = item.category_name || 'Tidak Berkategori'
|
||||
if (!acc[categoryName]) {
|
||||
acc[categoryName] = []
|
||||
}
|
||||
acc[categoryName].push(item)
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any[]>
|
||||
) || {}
|
||||
|
||||
const rows: JSX.Element[] = []
|
||||
let globalIndex = 0
|
||||
|
||||
// Sort categories alphabetically
|
||||
Object.keys(groupedProducts)
|
||||
.sort()
|
||||
.forEach(categoryName => {
|
||||
const categoryProducts = groupedProducts[categoryName]
|
||||
|
||||
// Category header row
|
||||
rows.push(
|
||||
<tr
|
||||
key={`category-${categoryName}`}
|
||||
className='bg-gray-100 border-b border-gray-300'
|
||||
style={{ pageBreakInside: 'avoid' }}
|
||||
>
|
||||
<td
|
||||
className='p-3 font-bold text-gray-900 border-r border-gray-300'
|
||||
style={{ color: '#36175e' }}
|
||||
>
|
||||
{categoryName.toUpperCase()}
|
||||
</td>
|
||||
<td className='p-3 border-r border-gray-300'></td>
|
||||
<td className='p-3 border-r border-gray-300'></td>
|
||||
<td className='p-3 border-r border-gray-300'></td>
|
||||
<td className='p-3'></td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
// Product rows for this category
|
||||
categoryProducts.forEach((item, index) => {
|
||||
globalIndex++
|
||||
rows.push(
|
||||
<tr
|
||||
key={`product-${item.product_name}-${index}`}
|
||||
className={`${globalIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'} border-b border-gray-200`}
|
||||
style={{ pageBreakInside: 'avoid' }}
|
||||
>
|
||||
<td
|
||||
className='p-3 pl-6 font-medium text-gray-800 border-r border-gray-200'
|
||||
style={{ wordWrap: 'break-word' }}
|
||||
>
|
||||
{item.product_name}
|
||||
</td>
|
||||
<td className='p-3 text-center text-gray-700 border-r border-gray-200'>
|
||||
{item.quantity_sold}
|
||||
</td>
|
||||
<td className='p-3 text-center text-gray-700 border-r border-gray-200'>
|
||||
{item.order_count ?? 0}
|
||||
</td>
|
||||
<td className='p-3 text-right font-semibold text-gray-800 border-r border-gray-200'>
|
||||
{formatCurrency(item.revenue)}
|
||||
</td>
|
||||
<td className='p-3 text-right font-medium text-gray-800'>
|
||||
{formatCurrency(item.average_price)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
// Category subtotal row
|
||||
const categoryTotalQty = categoryProducts.reduce(
|
||||
(sum, item) => sum + (item.quantity_sold || 0),
|
||||
0
|
||||
)
|
||||
const categoryTotalOrders = categoryProducts.reduce(
|
||||
(sum, item) => sum + (item.order_count || 0),
|
||||
0
|
||||
)
|
||||
const categoryTotalRevenue = categoryProducts.reduce((sum, item) => sum + (item.revenue || 0), 0)
|
||||
|
||||
rows.push(
|
||||
<tr
|
||||
key={`subtotal-${categoryName}`}
|
||||
className='bg-gray-200 border-b-2 border-gray-400'
|
||||
style={{ pageBreakInside: 'avoid' }}
|
||||
>
|
||||
<td className='p-3 pl-6 font-semibold text-gray-800 border-r border-gray-400'>
|
||||
Subtotal {categoryName}
|
||||
</td>
|
||||
<td className='p-3 text-center font-semibold text-gray-800 border-r border-gray-400'>
|
||||
{categoryTotalQty}
|
||||
</td>
|
||||
<td className='p-3 text-center font-semibold text-gray-800 border-r border-gray-400'>
|
||||
{categoryTotalOrders}
|
||||
</td>
|
||||
<td className='p-3 text-right font-semibold text-gray-800 border-r border-gray-400'>
|
||||
{formatCurrency(categoryTotalRevenue)}
|
||||
</td>
|
||||
<td className='p-3'></td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
return rows
|
||||
})()}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className='text-gray-800 border-t-2 border-gray-300' style={{ pageBreakInside: 'avoid' }}>
|
||||
<td className='p-3 font-bold border-r border-gray-300'>TOTAL KESELURUHAN</td>
|
||||
<td className='p-3 text-center font-bold border-r border-gray-300'>
|
||||
{productSummary.totalQuantitySold ?? 0}
|
||||
</td>
|
||||
<td className='p-3 text-center font-bold border-r border-gray-300'>
|
||||
{productSummary.totalOrders ?? 0}
|
||||
</td>
|
||||
<td className='p-3 text-right font-bold border-r border-gray-300'>
|
||||
{formatCurrency(productSummary.totalRevenue ?? 0)}
|
||||
</td>
|
||||
<td className='p-3'></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<ReportItemSubheader title='' />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReportSalesPerProductContent
|
||||
Loading…
x
Reference in New Issue
Block a user