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 { formatRupiah } from '@/lib/rupiah'; import { Head, router } from '@inertiajs/react'; import { Banknote, Package, ShoppingCart, UserCheck, } 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; }; myAttendance: { total_days: number; present_days: number; absent_days: number; leave_days: number; percentage: number; } | null; isManager: boolean; cashOverview: { total_balance: number; total_transactions: number; total_deposit: number; total_withdrawal: number; }; rawMaterialStock: { total_stock: number; total_value: number; by_unit: { yard: number; meter: number; kilogram: number; }; }; productStock: { total_stock: number; total_value: number; by_type: { 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; }>; }; const revenueChartConfig = { total: { label: 'Total', color: 'var(--chart-1)', }, gross: { label: 'Keuntungan Kotor', color: 'var(--chart-2)', }, net: { label: 'Keuntungan Bersih', color: 'var(--chart-6)', }, deduction: { label: 'Potongan Nego', color: 'var(--chart-3)', }, discount: { label: 'Diskon', color: 'var(--chart-4)', }, cogs: { label: 'HPP', color: 'var(--chart-5)', }, } satisfies ChartConfig; const revenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross', 'net'] as const; const expenseChartConfig = { total: { label: 'Total', color: 'var(--chart-1)', }, purchase: { label: 'Belanja', color: 'var(--chart-2)', }, expense: { label: 'Pengeluaran Toko', color: 'var(--chart-3)', }, advance: { label: 'Kasbon', color: 'var(--chart-4)', }, } satisfies ChartConfig; const expenseKeys = ['total', 'purchase', 'expense', 'advance'] as const; const revenueTrendChartConfig = { qty: { label: 'Qty', color: 'var(--chart-1)', }, } satisfies ChartConfig; const revenueTrendKeys = ['qty'] as const; const CHANNEL_COLORS: Record = { store: '#22c55e', shopee: '#ee4d2d', tiktok: '#000000', }; const PAYMENT_COLORS: Record = { cash: '#22c55e', transfer: '#60a5fa', qris: '#a855f7', marketplace: '#f97316', }; const PIE_COLORS = [ 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)', ]; 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 chartConfig = useMemo(() => { const config: ChartConfig = {}; data.forEach((item, index) => { config[item.name] = { label: item.name, color: PIE_COLORS[index % PIE_COLORS.length], }; }); return config; }, [data]); const chartData = useMemo(() => { return data.map((item) => ({ ...item, fill: PIE_COLORS[data.indexOf(item) % PIE_COLORS.length], })); }, [data]); return ( {title} {hasData ? ( } /> } 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, myAttendance, isManager, cashOverview, rawMaterialStock, productStock, revenueSummary, monthlyRevenue, monthlyRevenueByChannel, revenueByPaymentType, expenseSummary, monthlyExpense, busyHours, profitMetrics, topSuppliers, topCustomers, topProducts, marketingSales, orderStats, revenueTrend, }: 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 [activeExpenseKey, setActiveExpenseKey] = useState('total'); const hasActiveFilters = !!startDate || !!endDate; const formatDate = useCallback((date: Date): string => date.toISOString().split('T')[0], []); 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 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('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.attendance') && !isManager && myAttendance && ( )} {can('analysis.cash') && ( )} {can('analysis.raw_materials') && ( )} {can('analysis.product_stock') && ( )}
{can('analysis.revenue') && (
Pendapatan
{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') && (
({ 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 date = new Date(value); return date.toLocaleDateString('id-ID', { day: 'numeric', month: 'short' }); }} /> { return new Date(value).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 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
)} )}
); }