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',
|
iconClass: 'tabler-receipt-2',
|
||||||
link: getLocalizedUrl(`/apps/report/sales/sales-report`, locale as Locale)
|
link: getLocalizedUrl(`/apps/report/sales/sales-report`, locale as Locale)
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: 'Detail Penjualan',
|
// title: 'Detail Penjualan',
|
||||||
iconClass: 'tabler-receipt-2',
|
// iconClass: 'tabler-receipt-2',
|
||||||
link: ''
|
// link: ''
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: 'Tagihan Pelanggan',
|
// title: 'Tagihan Pelanggan',
|
||||||
iconClass: 'tabler-receipt-2',
|
// iconClass: 'tabler-receipt-2',
|
||||||
link: ''
|
// link: ''
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: 'Penjualan per Produk',
|
title: 'Penjualan per Produk',
|
||||||
iconClass: 'tabler-receipt-2',
|
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: ''
|
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// 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 (
|
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