import { StatCard } from '@/components/card/stat-card'; import { DatePicker } from '@/components/inputs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { useCan } from '@/hooks/use-can'; import { generateRandomColors } from '@/lib/utils'; import { formatRupiah } from '@/lib/rupiah'; import { format } from 'date-fns'; import { Head, router } from '@inertiajs/react'; import { Banknote, Package, ShoppingCart, } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Bar, BarChart, CartesianGrid, Cell, Line, LineChart, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, } from 'recharts'; type Filters = { start_date?: string; end_date?: string; }; type AnalysisProps = { filters: Filters; attendance: { total_employees: number; percentage: number; present: number; absent: number; on_leave: number; }; isManager: boolean; cashOverview: { total_balance: number; total_transactions: number; total_deposit: number; total_withdrawal: number; }; rawMaterialStock: { total_stock: number; total_price: number; by_unit: { yard: number; meter: number; kilogram: number; }; }; productStock: { total_stock: number; total_price: number; by_type: { stock?: number; reject_stock?: number; retail_stock?: number; }; }; revenueByStockType: { monthly: Array<{ month: string; good: number; reject: number; retail: number; }>; totals: { good: number; reject: number; retail: number; }; }; revenueSummary: { total_revenue: number; total_discount: number; total_deduction: number; cogs: number; gross: number; net: number; payroll_total: number; expense_total: number; total_orders: number; avg_order: number; }; monthlyRevenue: Array<{ month: string; total: number; gross: number; net: number; discount: number; deduction: number; cogs: number; payroll: number; expense: number; }>; monthlyRevenueByChannel: Array<{ month: string; store: number; shopee: number; tiktok: number; }>; revenueByPaymentType: Array<{ payment_type: string; label: string; total: number; }>; expenseSummary: { total: number; purchase_total: number; expense_total: number; advance_total: number; }; monthlyExpense: Array<{ month: string; total: number; purchase: number; expense: number; advance: number; }>; busyHours: Array<{ hour: string; orders: number; }>; profitMetrics: { total_orders: number; total_products_sold: number; hpp: number; gross_profit: number; net_profit: number; profit_margin: number; aov: number; items_per_transaction: number; payroll_total: number; expense_total: number; }; topSuppliers: Array<{ name: string; total_amount: number; purchase_count: number; }>; topCustomers: Array<{ name: string; total_amount: number; order_count: number; }>; topProducts: Array<{ name: string; total_qty: number; total_revenue: number; }>; marketingSales: Array<{ marketing_name: string; total_orders: number; total_products_sold: number; total_revenue: number; total_subtotal: number; total_discount: number; avg_order: number; }>; orderStats: { by_channel: Array<{ channel: string; label: string; count: number; total: number; }>; by_payment_type: Array<{ payment_type: string; label: string; count: number; total: number; }>; by_marketing: Array<{ name: string; count: number; total: number; }>; by_status: Array<{ status: string; label: string; count: number; }>; }; revenueTrend: Array<{ date: string; qty: number; }>; monthlyRetailRevenue: { monthly: Array<{ month: string; total: number; gross: number; discount: number; deduction: number; cogs: number; }>; summary: { total: number; discount: number; deduction: number; cogs: number; gross: number; }; }; }; const revenueChartConfig = (() => { const colors = generateRandomColors(6); return { total: { label: 'Total', color: colors[0] }, gross: { label: 'Keuntungan Kotor', color: colors[1] }, net: { label: 'Keuntungan Bersih', color: colors[2] }, deduction: { label: 'Potongan Nego', color: colors[3] }, discount: { label: 'Diskon', color: colors[4] }, cogs: { label: 'HPP', color: colors[5] }, } satisfies ChartConfig; })(); const revenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross', 'net'] as const; const retailRevenueChartConfig = (() => { const colors = generateRandomColors(5); return { total: { label: 'Total', color: colors[0] }, gross: { label: 'Keuntungan Kotor', color: colors[1] }, deduction: { label: 'Potongan Nego', color: colors[2] }, discount: { label: 'Diskon', color: colors[3] }, cogs: { label: 'HPP', color: colors[4] }, } satisfies ChartConfig; })(); const retailRevenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross'] as const; const expenseChartConfig = (() => { const colors = generateRandomColors(4); return { total: { label: 'Total', color: colors[0] }, purchase: { label: 'Belanja', color: colors[1] }, expense: { label: 'Pengeluaran Toko', color: colors[2] }, advance: { label: 'Kasbon', color: colors[3] }, } satisfies ChartConfig; })(); const expenseKeys = ['total', 'purchase', 'expense', 'advance'] as const; const revenueTrendChartConfig = (() => { const colors = generateRandomColors(1); return { qty: { label: 'Qty', color: colors[0] }, } satisfies ChartConfig; })(); const revenueTrendKeys = ['qty'] as const; const stockComparisonConfig = (() => { const colors = generateRandomColors(3); return { good: { label: 'Bagus', color: colors[0] }, reject: { label: 'Reject', color: colors[1] }, retail: { label: 'Ecer', color: colors[2] }, } satisfies ChartConfig; })(); const stockComparisonKeys = ['good', 'reject', 'retail'] as const; const CHANNEL_COLORS: Record = (() => { const colors = generateRandomColors(3); return { store: colors[0], shopee: colors[1], tiktok: colors[2], }; })(); const PAYMENT_COLORS: Record = (() => { const colors = generateRandomColors(4); return { cash: colors[0], transfer: colors[1], qris: colors[2], marketplace: colors[3], }; })(); type PieChartItem = { name: string; count: number; }; type DashboardPieChartProps = { title: string; data: PieChartItem[]; dataKey: string; nameKey: string; }; function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) { const hasData = data.length > 0 && data.some((d) => d.count > 0); const [activeName, setActiveName] = useState(undefined); const pieColors = useMemo(() => generateRandomColors(5), []); const chartConfig = useMemo(() => { const config: ChartConfig = {}; data.forEach((item, index) => { config[item.name] = { label: item.name, color: pieColors[index % pieColors.length], }; }); return config; }, [data, pieColors]); const chartData = useMemo(() => { return data.map((item) => ({ ...item, fill: pieColors[data.indexOf(item) % pieColors.length], })); }, [data, pieColors]); const handlePieClick = (_: unknown, index: number) => { const name = chartData[index]?.name; if (name) { setActiveName((prev) => (prev === name ? undefined : name)); } }; const handleLegendClick = (name: string) => { setActiveName((prev) => (prev === name ? undefined : name)); }; return ( {title} {hasData ? ( } /> {chartData.map((item, index) => ( ))} } className="-translate-y-2 flex-wrap gap-2 *:basis-1/4 *:justify-center" /> ) : (
Belum ada data
)}
); } type TooltipProps = { active?: boolean; payload?: Array<{ name: string; value: number; color?: string; payload: Record; }>; label?: string; }; function BarTooltip({ active, payload, label }: TooltipProps) { if (!active || !payload?.length) { return null; } return (

{label}

{payload.map((item, i) => (

{item.name} {typeof item.value === 'number' && item.value > 1000 ? `Rp${formatRupiah(item.value)}` : item.value}

))}
); } function DonutTooltip({ active, payload }: TooltipProps) { if (!active || !payload?.length) { return null; } const data = payload[0].payload; return (

{(data.label as string) ?? ''} {data.total !== undefined ? `Rp${formatRupiah(data.total as number)}` : (data.value as number)?.toLocaleString('id-ID')}

); } export default function Analysis({ filters: initialFilters, attendance, isManager, cashOverview, rawMaterialStock, productStock, revenueByStockType, revenueSummary, monthlyRevenue, monthlyRevenueByChannel, revenueByPaymentType, expenseSummary, monthlyExpense, busyHours, profitMetrics, topSuppliers, topCustomers, topProducts, marketingSales, orderStats, revenueTrend, monthlyRetailRevenue, }: AnalysisProps) { const { can, hasAnyRole, hasRole } = useCan(); const [startDate, setStartDate] = useState(initialFilters.start_date ?? ''); const [endDate, setEndDate] = useState(initialFilters.end_date ?? ''); const [selectedPreset, setSelectedPreset] = useState(''); const [activeRevenueKey, setActiveRevenueKey] = useState('total'); const [activeRetailRevenueKey, setActiveRetailRevenueKey] = useState('total'); const [activeExpenseKey, setActiveExpenseKey] = useState('total'); const [activeStockKey, setActiveStockKey] = useState<'good' | 'reject' | 'retail'>('good'); const hasActiveFilters = !!startDate || !!endDate; const formatDate = useCallback((date: Date): string => format(date, 'yyyy-MM-dd'), []); const applyFilters = useCallback(() => { router.get( '/admin/analysis', { start_date: startDate, end_date: endDate }, { preserveState: true, preserveScroll: true }, ); }, [startDate, endDate]); const clearFilters = useCallback(() => { setStartDate(''); setEndDate(''); setSelectedPreset(''); }, []); useEffect(() => { const timer = setTimeout(applyFilters, 500); return () => clearTimeout(timer); }, [startDate, endDate, applyFilters]); const onPresetChange = useCallback((value: string) => { setSelectedPreset(value); const now = new Date(); let start: Date; switch (value) { case 'today': start = new Date(now); break; case 'week': start = new Date(now); start.setDate(now.getDate() - now.getDay() + 1); break; case 'month': start = new Date(now.getFullYear(), now.getMonth(), 1); break; case 'year': start = new Date(now.getFullYear(), 0, 1); break; default: return; } setStartDate(formatDate(start)); setEndDate(formatDate(now)); }, [formatDate]); const revenueByChannelData = useMemo(() => { const totals = monthlyRevenueByChannel.reduce( (acc, item) => ({ store: acc.store + (item.store ?? 0), shopee: acc.shopee + (item.shopee ?? 0), tiktok: acc.tiktok + (item.tiktok ?? 0), }), { store: 0, shopee: 0, tiktok: 0 }, ); return [ { channel: 'store', label: 'Toko', total: totals.store, color: CHANNEL_COLORS.store }, { channel: 'shopee', label: 'Shopee', total: totals.shopee, color: CHANNEL_COLORS.shopee }, { channel: 'tiktok', label: 'TikTok', total: totals.tiktok, color: CHANNEL_COLORS.tiktok }, ]; }, [monthlyRevenueByChannel]); const stockComparisonData = useMemo(() => revenueByStockType.monthly, [revenueByStockType]); const peakHour = useMemo(() => { if (busyHours.length === 0) { return { hour: '-', orders: 0 }; } return busyHours.reduce((max, item) => (item.orders > max.orders ? item : max), busyHours[0]); }, [busyHours]); const visibleExpenseCharts = useMemo(() => { if (hasAnyRole(['owner', 'developer'])) { return expenseKeys; } return expenseKeys.filter((c) => c !== 'purchase'); }, [hasAnyRole]); const sectionOrder = useMemo(() => { if (hasAnyRole(['owner', 'developer'])) { return { statCards: 1, revenue: 4, revenueByChannel: 5, orderPie: 6, expense: 7, totalOrder: 8, revenueTrend: 9, marketingSales: 10, topSuppliers: 11, topProducts: 12, topCustomers: 13, busyHours: 14 }; } if (hasAnyRole(['admin_toko', 'direktur'])) { return { statCards: 1, revenue: 4, revenueByChannel: 5, orderPie: 6, expense: 7, totalOrder: 8, revenueTrend: 9, marketingSales: 10, topSuppliers: 11, topProducts: 12, topCustomers: 13, busyHours: 14 }; } if (hasRole('cashier')) { return { statCards: 1, revenue: 2, revenueByChannel: 3, orderPie: 4, totalOrder: 5, topProducts: 6, topCustomers: 7 }; } if (hasRole('marketing')) { return { statCards: 1, revenue: 2, revenueByChannel: 3, orderPie: 4, totalOrder: 5, revenueTrend: 6, topProducts: 7, topCustomers: 8 }; } return {}; }, [hasAnyRole, hasRole]); return ( <>

Analisa

Mulai: setStartDate(d ? formatDate(d) : '')} className="w-[170px]" placeholder="Pilih tanggal" />
Sampai: setEndDate(d ? formatDate(d) : '')} className="w-[170px]" placeholder="Pilih tanggal" />
{hasActiveFilters && ( )}
{can('analysis.cash') && ( )} {can('analysis.raw_materials') && ( )} {can('analysis.product_stock') && ( )}
{can('analysis.product_stock') && (
Pendapatan per Jenis Stok
{(['good', 'reject', 'retail'] as const).map((key) => { const value = revenueByStockType.totals[key]; return ( ); })}
{stockComparisonData.length > 0 ? ( ( <>
{stockComparisonConfig[activeStockKey]?.label ?? name} Rp{Number(value).toLocaleString('id-ID')} )} /> } /> ) : (
Belum ada data pendapatan
)} )} {can('analysis.revenue') && (
Pendapatan Global
{revenueKeys.filter((key) => { if (hasAnyRole(['owner', 'developer'])) return true; if (hasRole('cashier')) return key === 'total' || key === 'discount' || key === 'deduction' || key === 'cogs'; return key === 'total' || key === 'discount' || key === 'deduction' || key === 'cogs' || key === 'gross' || key === 'net'; }).map((key) => { const value = key === 'total' ? revenueSummary.total_revenue : key === 'discount' ? revenueSummary.total_discount : key === 'net' ? revenueSummary.net : key === 'gross' ? revenueSummary.gross : key === 'cogs' ? revenueSummary.cogs : revenueSummary.total_deduction; return ( ); })}
{monthlyRevenue.length > 0 ? ( ( <>
{revenueChartConfig[activeRevenueKey]?.label ?? name} Rp{Number(value).toLocaleString('id-ID')} )} /> } /> ) : (
Belum ada data pendapatan
)} )} {can('analysis.revenue') && (
Pendapatan per Channel
{revenueByChannelData.map((item) => (
{item.label} Rp{formatRupiah(item.total)}
))}
{revenueByChannelData.length > 0 ? (
{revenueByChannelData.map((entry, index) => ( ))} } />
) : (
Belum ada data pendapatan
)}
Pendapatan per Pembayaran
{revenueByPaymentType.map((item) => (
{item.label} Rp{formatRupiah(item.total)}
))}
{revenueByPaymentType.length > 0 ? (
({ ...item, color: PAYMENT_COLORS[item.payment_type] ?? '#94a3b8', }))} dataKey="total" nameKey="label" cx="50%" cy="50%" outerRadius={100} innerRadius={60} strokeWidth={2} stroke="#374151" > {revenueByPaymentType.map((entry, index) => ( ))} } />
) : (
Belum ada data pendapatan
)}
)} {can('analysis.revenue') && (
Pendapatan Penjualan Ecer
{retailRevenueKeys.map((key) => { const value = key === 'total' ? monthlyRetailRevenue.summary.total : key === 'discount' ? monthlyRetailRevenue.summary.discount : key === 'gross' ? monthlyRetailRevenue.summary.gross : key === 'cogs' ? monthlyRetailRevenue.summary.cogs : monthlyRetailRevenue.summary.deduction; return ( ); })}
{monthlyRetailRevenue.monthly.length > 0 ? ( ( <>
{retailRevenueChartConfig[activeRetailRevenueKey]?.label ?? name} Rp{Number(value).toLocaleString('id-ID')} )} /> } /> ) : (
Belum ada data pendapatan ecer
)} )} {can('analysis.revenue') && (
({ name: item.label, count: item.count, }))} dataKey="count" nameKey="name" /> ({ name: item.label, count: item.count, }))} dataKey="count" nameKey="name" /> ({ name: item.name, count: item.count, }))} dataKey="count" nameKey="name" /> ({ name: item.label, count: item.count, }))} dataKey="count" nameKey="name" />
)} {can('analysis.expense') && (
Pengeluaran
{visibleExpenseCharts.map((key) => { const value = key === 'total' ? expenseSummary.total : key === 'purchase' ? expenseSummary.purchase_total : key === 'expense' ? expenseSummary.expense_total : expenseSummary.advance_total; return ( ); })}
{monthlyExpense.length > 0 ? ( ( <>
{expenseChartConfig[activeExpenseKey]?.label ?? name} Rp{Number(value).toLocaleString('id-ID')} )} /> } /> ) : (
Belum ada data pengeluaran
)} )} {can('analysis.profit_orders') && (
)} {can('analysis.revenue_trend') && (
Trend Penjualan
{revenueTrendKeys.map((key) => { const total = revenueTrend.reduce((acc, item) => acc + (item[key] ?? 0), 0); return ( ); })}
{revenueTrend.length > 0 ? ( { const [year, month, day] = String(value).split('-').map(Number); const date = new Date(year, month - 1, day); return date.toLocaleDateString('id-ID', { day: 'numeric', month: 'short' }); }} /> { const [year, month, day] = String(value).split('-').map(Number); const date = new Date(year, month - 1, day); return date.toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' }); }} formatter={(value, name, item, index) => ( <>
{revenueTrendChartConfig[item.dataKey as keyof typeof revenueTrendChartConfig]?.label ?? name} {Number(value).toLocaleString('id-ID')} )} /> } /> ) : (
Belum ada data trend pendapatan
)} )} {can('analysis.marketing_sales') && ( Penjualan Marketing {marketingSales.length > 0 ? (
{marketingSales.map((item, index) => ( ))}
Marketing Total Order Produk Terjual Total Pendapatan Total Subtotal Total Diskon & Potongan Nego Rata-rata Order
{item.marketing_name} {item.total_orders.toLocaleString('id-ID')} {item.total_products_sold.toLocaleString('id-ID')} pcs Rp{formatRupiah(item.total_revenue)} Rp{formatRupiah(item.total_subtotal)} Rp{formatRupiah(item.total_discount)} Rp{formatRupiah(item.avg_order)}
) : (
Belum ada data penjualan marketing
)}
)} {can('analysis.top_suppliers') && ( Top 5 Supplier {topSuppliers.length > 0 ? ( ({ name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name, amount: s.total_amount }))} margin={{ left: 12, right: 12 }}> ( <>
Total Pembelian Rp{Number(value).toLocaleString('id-ID')} )} /> } /> ) : (
Belum ada data supplier
)} )} {can('analysis.top_products') && ( Top 5 Produk {topProducts.length > 0 ? ( ({ name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name, qty: p.total_qty }))} margin={{ left: 12, right: 12 }}> ( <>
Jumlah Terjual {Number(value).toLocaleString('id-ID')} )} /> } /> ) : (
Belum ada data produk
)} )} {can('analysis.top_customers') && ( Top 5 Pelanggan {topCustomers.length > 0 ? ( ({ name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name, amount: c.total_amount }))} margin={{ left: 12, right: 12 }}> ( <>
Total Pesanan Rp{Number(value).toLocaleString('id-ID')} )} /> } /> ) : (
Belum ada data pelanggan
)} )} {can('analysis.busy_hours') && (
Jam Sibuk Toko
Jam Tersibuk {peakHour.hour}
Pesanan {peakHour.orders.toLocaleString('id-ID')}
{busyHours.length > 0 ? ( ( <>
Pesanan {Number(value).toLocaleString('id-ID')} )} /> } /> ) : (
Belum ada data pesanan
)} )}
); }