From 073f3dd89cbcba1314d1aa790b5200fae7972aad Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 25 Sep 2025 23:05:26 +0700 Subject: [PATCH] Sales Per Product Report --- .../apps/report/sales/sales-product/page.tsx | 18 ++ src/views/apps/report/ReportSalesList.tsx | 52 ++--- .../ReportSalesPerProductContent.tsx | 207 ++++++++++++++++++ 3 files changed, 251 insertions(+), 26 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/report/sales/sales-product/page.tsx create mode 100644 src/views/apps/report/sales/sales-per-product/ReportSalesPerProductContent.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/report/sales/sales-product/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/report/sales/sales-product/page.tsx new file mode 100644 index 0000000..c905153 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/report/sales/sales-product/page.tsx @@ -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 ( + + + + + + + + + ) +} + +export default SalesProductReportPage diff --git a/src/views/apps/report/ReportSalesList.tsx b/src/views/apps/report/ReportSalesList.tsx index 36349da..aab3e7b 100644 --- a/src/views/apps/report/ReportSalesList.tsx +++ b/src/views/apps/report/ReportSalesList.tsx @@ -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 ( diff --git a/src/views/apps/report/sales/sales-per-product/ReportSalesPerProductContent.tsx b/src/views/apps/report/sales/sales-per-product/ReportSalesPerProductContent.tsx new file mode 100644 index 0000000..e3c3328 --- /dev/null +++ b/src/views/apps/report/sales/sales-per-product/ReportSalesPerProductContent.tsx @@ -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(new Date()) + const [endDate, setEndDate] = useState(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 ( + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + {(() => { + // 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 + ) || {} + + 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( + + + + + + + + ) + + // Product rows for this category + categoryProducts.forEach((item, index) => { + globalIndex++ + rows.push( + + + + + + + + ) + }) + + // 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( + + + + + + + + ) + }) + + return rows + })()} + + + + + + + + + + +
ProdukQtyOrderPendapatanRata Rata
+ {categoryName.toUpperCase()} +
+ {item.product_name} + + {item.quantity_sold} + + {item.order_count ?? 0} + + {formatCurrency(item.revenue)} + + {formatCurrency(item.average_price)} +
+ Subtotal {categoryName} + + {categoryTotalQty} + + {categoryTotalOrders} + + {formatCurrency(categoryTotalRevenue)} +
TOTAL KESELURUHAN + {productSummary.totalQuantitySold ?? 0} + + {productSummary.totalOrders ?? 0} + + {formatCurrency(productSummary.totalRevenue ?? 0)} +
+
+
+ +
+
+ ) +} + +export default ReportSalesPerProductContent