Sales Report PDF

This commit is contained in:
efrilm
2025-09-25 22:52:09 +07:00
parent e6bcf287ea
commit 07c0bdb3af
4 changed files with 1098 additions and 0 deletions
@@ -11,6 +11,11 @@ const ReportSalesList: React.FC = () => {
const { lang: locale } = useParams()
const salesReports = [
{
title: 'Penjualan',
iconClass: 'tabler-receipt-2',
link: getLocalizedUrl(`/apps/report/sales/sales-report`, locale as Locale)
},
{
title: 'Detail Penjualan',
iconClass: 'tabler-receipt-2',
@@ -0,0 +1,389 @@
'use client'
import DateRangePicker from '@/components/RangeDatePicker'
import { ReportItem, ReportItemFooter, ReportItemHeader, ReportItemSubheader } from '@/components/report/ReportItem'
import { PDFExportSalesService } from '@/services/export/pdf/PDFExportSalesService'
import {
useCategoryAnalytics,
usePaymentAnalytics,
useProductSalesAnalytics,
useProfitLossAnalytics
} from '@/services/queries/analytics'
import { formatCurrency, formatDateDDMMYYYY } from '@/utils/transform'
import { Button, Card, CardContent, Paper } from '@mui/material'
import { useState } from 'react'
const ReportSalesContent = () => {
const [startDate, setStartDate] = useState<Date | null>(new Date())
const [endDate, setEndDate] = useState<Date | null>(new Date())
const { data: profitLoss } = useProfitLossAnalytics({
date_from: formatDateDDMMYYYY(startDate!),
date_to: formatDateDDMMYYYY(endDate!)
})
const { data: paymentAnalytics } = usePaymentAnalytics({
date_from: formatDateDDMMYYYY(startDate!),
date_to: formatDateDDMMYYYY(endDate!)
})
const { data: category } = useCategoryAnalytics({
date_from: formatDateDDMMYYYY(startDate!),
date_to: formatDateDDMMYYYY(endDate!)
})
const { data: products } = useProductSalesAnalytics({
date_from: formatDateDDMMYYYY(startDate!),
date_to: formatDateDDMMYYYY(endDate!)
})
const categorySummary = {
totalRevenue: category?.data?.reduce((sum, item) => sum + (item?.total_revenue || 0), 0) || 0,
orderCount: category?.data?.reduce((sum, item) => sum + (item?.order_count || 0), 0) || 0,
productCount: category?.data?.reduce((sum, item) => sum + (item?.product_count || 0), 0) || 0,
totalQuantity: category?.data?.reduce((sum, item) => sum + (item?.total_quantity || 0), 0) || 0
}
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
}
const handleExportPDF = async () => {
try {
const salesData = {
profitLoss: profitLoss!,
paymentAnalytics: paymentAnalytics!,
categoryAnalytics: category!,
productAnalytics: products!
}
const result = await PDFExportSalesService.exportSalesReportToPDF(salesData)
if (result.success) {
console.log('PDF export successful:', result.filename)
// Optional: Show success notification
} else {
console.error('PDF export failed:', result.error)
alert('Export PDF gagal. Silakan coba lagi.')
}
} catch (error) {
console.error('PDF export error:', error)
alert('Terjadi kesalahan saat export PDF.')
}
}
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'
date={`${profitLoss?.date_from.split('T')[0]} - ${profitLoss?.date_to.split('T')[0]}`}
/>
<ReportItem
accountCode=''
accountName='Total Penjualan'
amount={profitLoss?.summary.total_revenue ?? 0}
onClick={() => {}}
/>
<ReportItem
accountCode=''
accountName='Total Diskon'
amount={profitLoss?.summary.total_discount ?? 0}
onClick={() => {}}
/>
<ReportItem
accountCode=''
accountName='Total Pajak'
amount={profitLoss?.summary.total_tax ?? 0}
onClick={() => {}}
/>
<ReportItemFooter title='Total' amount={profitLoss?.summary.total_revenue ?? 0} />
<ReportItemSubheader title='' />
<ReportItemHeader
title='Invoice'
date={`${profitLoss?.date_from.split('T')[0]} - ${profitLoss?.date_to.split('T')[0]}`}
/>
<ReportItem
accountCode=''
accountName='Total Invoice'
amount={profitLoss?.summary.total_orders ?? 0}
onClick={() => {}}
/>
<ReportItem
accountCode=''
accountName='Rata-rata Tagihan per Invoice'
amount={profitLoss?.summary.average_profit ?? 0}
onClick={() => {}}
/>
<ReportItemSubheader title='' />
<ReportItemHeader
title='Ringkasan Metode Pembayaran'
date={`${profitLoss?.date_from.split('T')[0]} - ${profitLoss?.date_to.split('T')[0]}`}
/>
<div className='bg-gray-50 border border-gray-200 overflow-hidden'>
<table className='w-full'>
<thead>
<tr className='text-gray-800 border-b-2 border-gray-300'>
<th className='text-left p-3 font-semibold'>Metode Pembayaran</th>
<th className='text-center p-3 font-semibold'>Tipe</th>
<th className='text-center p-3 font-semibold'>Jumlah Order</th>
<th className='text-right p-3 font-semibold'>Total Amount</th>
<th className='text-center p-3 font-semibold'>Persentase</th>
</tr>
</thead>
<tbody>
{paymentAnalytics?.data?.map((payment, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className='p-3 font-medium text-gray-800'>{payment.payment_method_name}</td>
<td className='p-3 text-center'>
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
payment.payment_method_type === 'cash'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}
>
{payment.payment_method_type.toUpperCase()}
</span>
</td>
<td className='p-3 text-center text-gray-700'>{payment.order_count}</td>
<td className='p-3 text-right font-semibold text-gray-800'>{formatCurrency(payment.total_amount)}</td>
<td className='p-3 text-center font-medium' style={{ color: '#36175e' }}>
{(payment.percentage ?? 0).toFixed(1)}%
</td>
</tr>
)) || []}
</tbody>
<tfoot>
<tr className='text-gray-800 border-t-2 border-gray-300'>
<td className='p-3 font-bold'>TOTAL</td>
<td className='p-3'></td>
<td className='p-3 text-center font-bold'>{paymentAnalytics?.summary.total_orders ?? 0}</td>
<td className='p-3 text-right font-bold'>
{formatCurrency(paymentAnalytics?.summary.total_amount ?? 0)}
</td>
<td className='p-3 text-center font-bold'></td>
</tr>
</tfoot>
</table>
</div>
<ReportItemSubheader title='' />
<ReportItemHeader
title='Ringkasan Kategori'
date={`${profitLoss?.date_from.split('T')[0]} - ${profitLoss?.date_to.split('T')[0]}`}
/>
<div className='bg-gray-50 border border-gray-200 overflow-hidden'>
<table className='w-full'>
<thead>
<tr className='text-gray-800 border-b-2 border-gray-300'>
<th className='text-left p-3 font-semibold'>Nama</th>
<th className='text-center p-3 font-semibold'>Total Produk</th>
<th className='text-center p-3 font-semibold'>Qty</th>
<th className='text-right p-3 font-semibold'>Pendapatan</th>
</tr>
</thead>
<tbody>
{category?.data?.map((c, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className='p-3 font-medium text-gray-800'>{c.category_name}</td>
<td className='p-3 text-center text-gray-700'>{c.product_count}</td>
<td className='p-3 text-center text-gray-700'>{c.total_quantity}</td>
<td className='p-3 text-right font-semibold' style={{ color: '#36175e' }}>
{formatCurrency(c.total_revenue)}
</td>
</tr>
)) || []}
</tbody>
<tfoot>
<tr className='text-gray-800 border-t-2 border-gray-300'>
<td className='p-3 font-bold'>TOTAL</td>
<td className='p-3 text-center font-bold'>{categorySummary?.productCount ?? 0}</td>
<td className='p-3 text-center font-bold'>{categorySummary?.totalQuantity ?? 0}</td>
<td className='p-3 text-right font-bold'>{formatCurrency(categorySummary?.totalRevenue ?? 0)}</td>
</tr>
</tfoot>
</table>
</div>
<ReportItemSubheader title='' />
<ReportItemHeader
title='Ringkasan Item'
date={`${profitLoss?.date_from.split('T')[0]} - ${profitLoss?.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 ReportSalesContent