From 7a8bd59bb47fe22369ac156aecde89d6bd4500d4 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 14 Aug 2025 02:07:55 +0700 Subject: [PATCH 1/2] feat: add categories at daily report --- .../dashboards/daily-report/page.tsx | 62 +++++++- src/services/queries/analytics.ts | 39 ++++- src/types/services/analytic.ts | 139 ++++++++++-------- 3 files changed, 173 insertions(+), 67 deletions(-) diff --git a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx index da88704..ef07537 100644 --- a/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx +++ b/src/app/[lang]/(dashboard)/(private)/dashboards/daily-report/page.tsx @@ -4,7 +4,8 @@ import { useProductSalesAnalytics, useProfitLossAnalytics, useSalesAnalytics, - usePaymentAnalytics + usePaymentAnalytics, + useCategoryAnalytics } from '@/services/queries/analytics' import { useOutletById } from '@/services/queries/outlets' import { formatCurrency, formatDate, formatDateDDMMYYYY, formatDatetime } from '@/utils/transform' @@ -45,6 +46,7 @@ const DailyPOSReport = () => { const { data: profitLoss } = useProfitLossAnalytics(dateParams) const { data: products } = useProductSalesAnalytics(dateParams) const { data: paymentAnalytics } = usePaymentAnalytics(dateParams) + const { data: category } = useCategoryAnalytics(dateParams) const productSummary = { totalQuantitySold: products?.data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0, @@ -60,6 +62,13 @@ const DailyPOSReport = () => { totalQuantity: profitLoss?.product_data?.reduce((sum, item) => sum + (item?.quantity_sold || 0), 0) || 0 } + 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 + } + useEffect(() => { setNow(new Date()) }, []) @@ -199,13 +208,13 @@ const DailyPOSReport = () => { {/* Performance Summary */}

- 1. Ringkasan Kinerja + 1. Ringkasan

- TOTAL PENJUALAN (termasuk rasik) + Total Penjualan (termasuk rasik) {formatCurrency(profitLoss?.summary.total_revenue ?? 0)} @@ -359,7 +368,50 @@ const DailyPOSReport = () => { {formatCurrency(paymentAnalytics?.summary.total_amount ?? 0)} - 100.0% + + + + +
+
+ + {/* Category Summary */} +
+

+ 2. Ringkasan Kategori +

+ +
+ + + + + + + + + + + + {category?.data?.map((c, index) => ( + + + + + + + + )) || []} + + + + + + + +
NamaTotal ProdukQtyJumlah OrderPendapatan
{c.category_name}{c.product_count}{c.total_quantity}{c.order_count} + {formatCurrency(c.total_revenue)} +
TOTAL{categorySummary?.productCount ?? 0}{categorySummary?.totalQuantity ?? 0}{categorySummary?.orderCount ?? 0}{formatCurrency(categorySummary?.totalRevenue ?? 0)}
@@ -369,7 +421,7 @@ const DailyPOSReport = () => { {/* Transaction Summary */}

- 3. Ringkasan Transaksi + 4. Ringkasan Item

diff --git a/src/services/queries/analytics.ts b/src/services/queries/analytics.ts index 27e30bf..f169c28 100644 --- a/src/services/queries/analytics.ts +++ b/src/services/queries/analytics.ts @@ -1,5 +1,12 @@ import { useQuery } from '@tanstack/react-query' -import { DashboardReport, PaymentReport, ProductSalesReport, ProfitLossReport, SalesReport } from '../../types/services/analytic' +import { + CategoryReport, + DashboardReport, + PaymentReport, + ProductSalesReport, + ProfitLossReport, + SalesReport +} from '../../types/services/analytic' import { api } from '../api' import { formatDateDDMMYYYY } from '../../utils/transform' @@ -157,3 +164,33 @@ export function useProfitLossAnalytics(params: AnalyticQueryParams = {}) { } }) } + +export function useCategoryAnalytics(params: AnalyticQueryParams = {}) { + const today = new Date() + const monthAgo = new Date() + monthAgo.setDate(today.getDate() - 30) + + const defaultDateTo = formatDateDDMMYYYY(today) + const defaultDateFrom = formatDateDDMMYYYY(monthAgo) + + const { date_from = defaultDateFrom, date_to = defaultDateTo, ...filters } = params + + return useQuery({ + queryKey: ['analytics-categories', { date_from, date_to, ...filters }], + queryFn: async () => { + const queryParams = new URLSearchParams() + + queryParams.append('date_from', date_from) + queryParams.append('date_to', date_to) + + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + queryParams.append(key, value.toString()) + } + }) + + const res = await api.get(`/analytics/categories?${queryParams.toString()}`) + return res.data.data + } + }) +} diff --git a/src/types/services/analytic.ts b/src/types/services/analytic.ts index 6f057ce..a978ccf 100644 --- a/src/types/services/analytic.ts +++ b/src/types/services/analytic.ts @@ -1,31 +1,31 @@ export interface SalesSummary { - total_sales: number; - total_orders: number; - total_items: number; - average_order_value: number; - total_tax: number; - total_discount: number; - net_sales: number; + total_sales: number + total_orders: number + total_items: number + average_order_value: number + total_tax: number + total_discount: number + net_sales: number } export interface SalesDataItem { - date: string; // ISO string, e.g., "2025-08-03T00:00:00Z" - sales: number; - orders: number; - items: number; - tax: number; - discount: number; - net_sales: number; + date: string // ISO string, e.g., "2025-08-03T00:00:00Z" + sales: number + orders: number + items: number + tax: number + discount: number + net_sales: number } export interface SalesReport { - organization_id: string; - outlet_id: string; - date_from: string; // ISO string with timezone, e.g., "2025-08-01T00:00:00+07:00" - date_to: string; // ISO string with timezone - group_by: string; // e.g., "day", "month", etc. - summary: SalesSummary; - data: SalesDataItem[]; + organization_id: string + outlet_id: string + date_from: string // ISO string with timezone, e.g., "2025-08-01T00:00:00+07:00" + date_to: string // ISO string with timezone + group_by: string // e.g., "day", "month", etc. + summary: SalesSummary + data: SalesDataItem[] } export interface ProductData { @@ -105,53 +105,70 @@ export type DashboardReport = { } export interface ProfitLossReport { - organization_id: string; - date_from: string; // ISO date string with timezone - date_to: string; // ISO date string with timezone - group_by: string; - summary: Summary; - data: DailyData[]; - product_data: ProductDataReport[]; + organization_id: string + date_from: string // ISO date string with timezone + date_to: string // ISO date string with timezone + group_by: string + summary: Summary + data: DailyData[] + product_data: ProductDataReport[] } export interface Summary { - total_revenue: number; - total_cost: number; - gross_profit: number; - gross_profit_margin: number; - total_tax: number; - total_discount: number; - net_profit: number; - net_profit_margin: number; - total_orders: number; - average_profit: number; - profitability_ratio: number; + total_revenue: number + total_cost: number + gross_profit: number + gross_profit_margin: number + total_tax: number + total_discount: number + net_profit: number + net_profit_margin: number + total_orders: number + average_profit: number + profitability_ratio: number } export interface DailyData { - date: string; // ISO date string with timezone - revenue: number; - cost: number; - gross_profit: number; - gross_profit_margin: number; - tax: number; - discount: number; - net_profit: number; - net_profit_margin: number; - orders: number; + date: string // ISO date string with timezone + revenue: number + cost: number + gross_profit: number + gross_profit_margin: number + tax: number + discount: number + net_profit: number + net_profit_margin: number + orders: number } export interface ProductDataReport { - product_id: string; - product_name: string; - category_id: string; - category_name: string; - quantity_sold: number; - revenue: number; - cost: number; - gross_profit: number; - gross_profit_margin: number; - average_price: number; - average_cost: number; - profit_per_unit: number; + product_id: string + product_name: string + category_id: string + category_name: string + quantity_sold: number + revenue: number + cost: number + gross_profit: number + gross_profit_margin: number + average_price: number + average_cost: number + profit_per_unit: number +} + +export interface CategoryReport { + organization_id: string + outlet_id: string + date_from: string + date_to: string + data: CategoryDataReport[] +} + +export interface CategoryDataReport { + category_id: string + category_name: string + total_revenue: number + total_quantity: number + product_count: number + order_count: number } From 16ad569297442000452422e9d288ba07323f5957 Mon Sep 17 00:00:00 2001 From: efrilm Date: Thu, 14 Aug 2025 03:17:17 +0700 Subject: [PATCH 2/2] feat: report inventory --- .../apps/inventory/stock/export/page.tsx | 317 ++++++++++++++++++ src/services/queries/analytics.ts | 41 +++ src/types/services/analytic.ts | 53 +++ .../ecommerce/stock/list/StockListTable.tsx | 25 +- 4 files changed, 428 insertions(+), 8 deletions(-) create mode 100644 src/app/[lang]/(dashboard)/(private)/apps/inventory/stock/export/page.tsx diff --git a/src/app/[lang]/(dashboard)/(private)/apps/inventory/stock/export/page.tsx b/src/app/[lang]/(dashboard)/(private)/apps/inventory/stock/export/page.tsx new file mode 100644 index 0000000..f087a27 --- /dev/null +++ b/src/app/[lang]/(dashboard)/(private)/apps/inventory/stock/export/page.tsx @@ -0,0 +1,317 @@ +'use client' + +import { useInventoryAnalytics } from '@/services/queries/analytics' +import { useOutletById } from '@/services/queries/outlets' +import { formatDateDDMMYYYY } from '@/utils/transform' +import ReportGeneratorComponent from '@/views/dashboards/daily-report/report-generator' +import ReportHeader from '@/views/dashboards/daily-report/report-header' +import { useRef, useState } from 'react' +const ExportInventoryPage = () => { + const reportRef = useRef(null) + const [now, setNow] = useState(new Date()) + const [selectedDate, setSelectedDate] = useState(new Date()) + const [dateRange, setDateRange] = useState({ + startDate: new Date(), + endDate: new Date() + }) + const [filterType, setFilterType] = useState<'single' | 'range'>('single') // 'single' or 'range' + + const getDateParams = () => { + if (filterType === 'single') { + return { + date_from: formatDateDDMMYYYY(selectedDate), + date_to: formatDateDDMMYYYY(selectedDate) + } + } else { + return { + date_from: formatDateDDMMYYYY(dateRange.startDate), + date_to: formatDateDDMMYYYY(dateRange.endDate) + } + } + } + + const dateParams = getDateParams() + + const { data: outlet } = useOutletById() + const { data: inventory } = useInventoryAnalytics(dateParams) + + const handleGeneratePDF = async () => { + const reportElement = reportRef.current + + try { + // Import jsPDF dan html2canvas + const jsPDF = (await import('jspdf')).default + const html2canvas = (await import('html2canvas')).default + + // Optimized canvas capture dengan scale lebih rendah + const canvas = await html2canvas(reportElement!, { + scale: 1.5, // Reduced from 2 to 1.5 + useCORS: true, + allowTaint: true, + backgroundColor: '#ffffff', + logging: false, // Disable logging for performance + removeContainer: true, // Clean up after capture + imageTimeout: 0, // No timeout for image loading + height: reportElement!.scrollHeight, + width: reportElement!.scrollWidth + }) + + // Compress canvas using JPEG with quality setting + const imgData = canvas.toDataURL('image/jpeg', 0.85) // JPEG with 85% quality instead of PNG + + // Create PDF with compression + const pdf = new jsPDF({ + orientation: 'portrait', + unit: 'mm', + format: 'a4', + compress: true // Enable built-in compression + }) + + const imgWidth = 210 + const pageHeight = 295 + const imgHeight = (canvas.height * imgWidth) / canvas.width + let heightLeft = imgHeight + let position = 0 + + // Add first page with compressed image + pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, '', 'FAST') // Use FAST compression + heightLeft -= pageHeight + + // Handle multiple pages if needed + while (heightLeft >= 0) { + position = heightLeft - imgHeight + pdf.addPage() + pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight, '', 'FAST') + heightLeft -= pageHeight + } + + // Additional compression options + pdf.setProperties({ + title: `Laporan Inventori`, + subject: 'Laporan Inventori', + author: 'Apskel POS System', + creator: 'Apskel' + }) + + // Save with optimized settings + const fileName = + filterType === 'single' + ? `laporan-inventory-${formatDateForInput(selectedDate)}.pdf` + : `laporan-inventory-${formatDateForInput(dateRange.startDate)}-to-${formatDateForInput(dateRange.endDate)}.pdf` + + pdf.save(fileName, { + returnPromise: true + }) + + // Clean up canvas to free memory + canvas.width = canvas.height = 0 + } catch (error) { + console.error('Error generating PDF:', error) + alert('Terjadi kesalahan saat membuat PDF. Pastikan jsPDF dan html2canvas sudah terinstall.') + } + } + + const formatDateForInput = (date: Date) => { + return date.toISOString().split('T')[0] + } + + const getReportPeriodText = () => { + if (filterType === 'single') { + return `${formatDateDDMMYYYY(selectedDate)} - ${formatDateDDMMYYYY(selectedDate)}` + } else { + return `${formatDateDDMMYYYY(dateRange.startDate)} - ${formatDateDDMMYYYY(dateRange.endDate)}` + } + } + + const productSummary = { + totalQuantity: inventory?.products?.reduce((sum, item) => sum + (item?.quantity || 0), 0) || 0, + totalIn: inventory?.products?.reduce((sum, item) => sum + (item?.total_in || 0), 0) || 0, + totalOut: inventory?.products?.reduce((sum, item) => sum + (item?.total_out || 0), 0) || 0 + } + + const ingredientSummary = { + totalQuantity: inventory?.ingredients?.reduce((sum, item) => sum + (item?.quantity || 0), 0) || 0, + totalIn: inventory?.ingredients?.reduce((sum, item) => sum + (item?.total_in || 0), 0) || 0, + totalOut: inventory?.ingredients?.reduce((sum, item) => sum + (item?.total_out || 0), 0) || 0 + } + + return ( +
+ {/* Control Panel */} + +
+ {/* Header */} + + + {/* Ringkasan */} +
+

+ 1. Ringkasan +

+ +
+
+
+ Total Item + {inventory?.summary.total_products} +
+
+ Total Item Masuk + {inventory?.summary.low_stock_products} +
+
+ Total Item Keluar + {inventory?.summary.zero_stock_products} +
+
+ +
+
+ Total Ingredient + {inventory?.summary.total_ingredients} +
+
+ Total Ingredient Masuk + {inventory?.summary.low_stock_ingredients} +
+
+ Total Ingredient Keluar + {inventory?.summary.zero_stock_ingredients} +
+
+
+
+ + {/* Item */} +
+

+ 2. Item +

+ +
+ + + + + + + + + + + + {inventory?.products?.map((product, index) => { + let rowClass = index % 2 === 0 ? 'bg-white' : 'bg-gray-50' + + if (product.is_zero_stock) { + rowClass = 'bg-red-300' // Merah untuk stok habis + } else if (product.is_low_stock) { + rowClass = 'bg-yellow-300' // Kuning untuk stok sedikit + } + return ( + + + + + + + + + ) + }) || []} + + + + + + + + + + +
NamaKategoriStockMasukKeluar
{product.product_name}{product.category_name}{product.quantity}{product.total_in}{product.total_out}
TOTAL{productSummary.totalQuantity ?? 0}{productSummary.totalIn ?? 0}{productSummary.totalOut ?? 0}
+
+
+ + {/* Ingredient */} +
+

+ 3. Ingredient +

+ +
+ + + + + + + + + + + {inventory?.ingredients?.map((ingredient, index) => { + let rowClass = index % 2 === 0 ? 'bg-white' : 'bg-gray-50' + + if (ingredient.is_zero_stock) { + rowClass = 'bg-red-300' // Merah untuk stok habis + } else if (ingredient.is_low_stock) { + rowClass = 'bg-yellow-300' // Kuning untuk stok sedikit + } + return ( + + + + + + + ) + }) || []} + + + + + + + + + +
NamaStockMasukKeluar
{ingredient.ingredient_name}{ingredient.quantity}{ingredient.total_in}{ingredient.total_out}
TOTAL{ingredientSummary.totalQuantity ?? 0}{ingredientSummary.totalIn ?? 0}{ingredientSummary.totalOut ?? 0}
+
+
+ + {/* Footer */} +
+
+

© 2025 Apskel - Sistem POS Terpadu

+

+

Dicetak pada: {now.toLocaleDateString('id-ID')}

+
+
+
+
+ ) +} + +export default ExportInventoryPage diff --git a/src/services/queries/analytics.ts b/src/services/queries/analytics.ts index f169c28..b636956 100644 --- a/src/services/queries/analytics.ts +++ b/src/services/queries/analytics.ts @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query' import { CategoryReport, DashboardReport, + InventoryReport, PaymentReport, ProductSalesReport, ProfitLossReport, @@ -194,3 +195,43 @@ export function useCategoryAnalytics(params: AnalyticQueryParams = {}) { } }) } + +export function useInventoryAnalytics(params: AnalyticQueryParams = {}) { + const today = new Date() + const monthAgo = new Date() + monthAgo.setDate(today.getDate() - 30) + + const defaultDateTo = formatDateDDMMYYYY(today) + const defaultDateFrom = formatDateDDMMYYYY(monthAgo) + + const { date_from = defaultDateFrom, date_to = defaultDateTo, ...filters } = params + + const user = (() => { + try { + return JSON.parse(localStorage.getItem('user') || '{}') + } catch { + return {} + } + })() + + const outletId = user?.outlet_id // + + return useQuery({ + queryKey: ['analytics-inventory', { date_from, date_to, ...filters }], + queryFn: async () => { + const queryParams = new URLSearchParams() + + queryParams.append('date_from', date_from) + queryParams.append('date_to', date_to) + + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + queryParams.append(key, value.toString()) + } + }) + + const res = await api.get(`/inventory/report/details/${outletId}?${queryParams.toString()}`) + return res.data.data + } + }) +} diff --git a/src/types/services/analytic.ts b/src/types/services/analytic.ts index a978ccf..6b49b0e 100644 --- a/src/types/services/analytic.ts +++ b/src/types/services/analytic.ts @@ -172,3 +172,56 @@ export interface CategoryDataReport { product_count: number order_count: number } + +export interface InventoryReport { + summary: InventorySummaryReport + products: InventoryProductReport[] + ingredients: InventoryIngredientReport[] +} + +export interface InventorySummaryReport { + total_products: number + total_ingredients: number + total_value: number + low_stock_products: number + low_stock_ingredients: number + zero_stock_products: number + zero_stock_ingredients: number + total_sold_products: number + total_sold_ingredients: number + outlet_id: string + outlet_name: string + generated_at: string +} + +export interface InventoryProductReport { + id: string + product_id: string + product_name: string + category_name: string + quantity: number + reorder_level: number + unit_cost: number + total_value: number + total_in: number + total_out: number + is_low_stock: boolean + is_zero_stock: boolean + updated_at: string +} + +export interface InventoryIngredientReport { + id: string + ingredient_id: string + ingredient_name: string + unit_name: string + quantity: number + reorder_level: number + unit_cost: number + total_value: number + total_in: number + total_out: number + is_low_stock: boolean + is_zero_stock: boolean + updated_at: string +} diff --git a/src/views/apps/ecommerce/stock/list/StockListTable.tsx b/src/views/apps/ecommerce/stock/list/StockListTable.tsx index ecbec50..7064946 100644 --- a/src/views/apps/ecommerce/stock/list/StockListTable.tsx +++ b/src/views/apps/ecommerce/stock/list/StockListTable.tsx @@ -41,6 +41,10 @@ import { useInventoriesMutation } from '../../../../../services/mutations/invent import { useInventories } from '../../../../../services/queries/inventories' import { Inventory } from '../../../../../types/services/inventory' import AddStockDrawer from './AddStockDrawer' +import Link from 'next/link' +import { getLocalizedUrl } from '@/utils/i18n' +import { Locale } from '@/configs/i18n' +import { useParams } from 'next/navigation' declare module '@tanstack/table-core' { interface FilterFns { @@ -109,6 +113,9 @@ const StockListTable = () => { const [addInventoryOpen, setAddInventoryOpen] = useState(false) const [search, setSearch] = useState('') + // Hooks + const { lang: locale } = useParams() + // Fetch products with pagination and search const { data, isLoading, error, isFetching } = useInventories({ page: currentPage, @@ -259,14 +266,16 @@ const StockListTable = () => { 25 50 - + + +