import { Head, router } from '@inertiajs/react'; import { Banknote, Package, ShoppingCart, TrendingUp, UserCheck, } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts'; import { StatCard } from '@/components/card/stat-card'; import { DatePicker } from '@/components/inputs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { useCan } from '@/hooks/use-can'; import { formatRupiah, formatRupiahShort } from '@/lib/rupiah'; 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_reject: number; total_retail: number; total_value: number; total_products: number; total_variants: number; total_categories: number; }; revenueSummary: { total_revenue: number; total_discount: number; total_marketplace_fees: number; total_deduction: number; total_orders: number; avg_order: number; }; monthlyRevenue: Array<{ month: string; total: number; net: number; net_warehouse: number; net_retail: number; deduction: 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; }; 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; }>; }; const REVENUE_COLORS: Record = { total: '#60a5fa', net: '#22c55e', net_warehouse: '#10b981', net_retail: '#06b6d4', deduction: '#f97316', }; const EXPENSE_COLORS: Record = { total: '#60a5fa', purchase: '#f97316', expense: '#a855f7', advance: '#ef4444', }; const CHANNEL_COLORS: Record = { store: '#22c55e', shopee: '#ee4d2d', tiktok: '#000000', }; const PAYMENT_COLORS: Record = { cash: '#22c55e', transfer: '#60a5fa', qris: '#a855f7', marketplace: '#f97316', }; 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, }: 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 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(() => { const charts = ['total', 'purchase', 'expense', 'advance'] as const; if (hasAnyRole(['owner', 'developer'])) { return charts; } return charts.filter((c) => c !== 'purchase'); }, [hasAnyRole]); const sectionOrder = useMemo(() => { if (hasAnyRole(['owner', 'developer'])) { return { statCards: 1, revenue: 5, revenueByChannel: 6, expense: 7, profitGross: 8, totalOrder: 9, marketingSales: 10, topSuppliers: 11, topProducts: 12, topCustomers: 13, busyHours: 14 }; } if (hasAnyRole(['admin_toko', 'direktur'])) { return { statCards: 1, revenue: 4, revenueByChannel: 5, expense: 6, profitGross: 7, totalOrder: 8, marketingSales: 9, topProducts: 10, topCustomers: 11, busyHours: 12 }; } if (hasRole('marketing')) { return { statCards: 1, revenue: 2, revenueByChannel: 3, totalOrder: 4, topProducts: 5, topCustomers: 6 }; } 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 && ( )} {can('analysis.attendance') && !isManager && myAttendance && ( )} {can('analysis.cash') && ( )} {can('analysis.raw_materials') && ( )} {can('analysis.product_stock') && ( )}
{can('analysis.revenue') && (
Pendapatan
{Object.entries(REVENUE_COLORS).filter(([key]) => { if (hasAnyRole(['owner', 'developer'])) { return true; } if (hasRole('cashier')) { return key === 'total' || key === 'deduction'; } return key === 'total' || key === 'net' || key === 'deduction'; }).map(([key]) => (
{key === 'net_warehouse' ? 'Total Gudang' : key === 'net_retail' ? 'Total Ecer' : key === 'total' ? 'Total' : key === 'net' ? 'Bersih' : 'Potongan'} Rp{formatRupiah(key === 'total' ? revenueSummary.total_revenue : key === 'deduction' ? revenueSummary.total_deduction : 0)}
))}
{monthlyRevenue.length > 0 ? ( `Rp${formatRupiahShort(v)}`} /> } /> ) : (
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.expense') && (
Pengeluaran
{visibleExpenseCharts.map((chart) => (
{chart === 'total' ? 'Total' : chart === 'purchase' ? 'Belanja' : chart === 'expense' ? 'Pengeluaran' : 'Kasbon'} Rp{formatRupiah(chart === 'total' ? expenseSummary.total : chart === 'purchase' ? expenseSummary.purchase_total : chart === 'expense' ? expenseSummary.expense_total : expenseSummary.advance_total)}
))}
{monthlyExpense.length > 0 ? ( `Rp${formatRupiahShort(v)}`} /> } /> ) : (
Belum ada data pengeluaran
)}
)} {(can('analysis.profit_gross') || can('analysis.profit_hpp')) && (
)} {can('analysis.profit_orders') && (
)} {can('analysis.marketing_sales') && ( Penjualan Marketing Rekap penjualan per 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 Berdasarkan total harga pembelian {topSuppliers.length > 0 ? ( ({ name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name, amount: s.total_amount }))}> `Rp${formatRupiahShort(v)}`} /> } /> ) : (
Belum ada data supplier
)}
)} {can('analysis.top_products') && ( Top 5 Produk Berdasarkan jumlah terjual {topProducts.length > 0 ? ( ({ name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name, qty: p.total_qty }))}> } /> ) : (
Belum ada data produk
)}
)} {can('analysis.top_customers') && ( Top 5 Pelanggan Berdasarkan total nilai pesanan {topCustomers.length > 0 ? ( ({ name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name, amount: c.total_amount }))}> `Rp${formatRupiahShort(v)}`} /> } /> ) : (
Belum ada data pelanggan
)}
)} {can('analysis.busy_hours') && (
Jam Sibuk Toko

Jam Tersibuk

{peakHour.hour}

{peakHour.orders} pesanan

{busyHours.length > 0 ? ( Math.round(v).toString()} /> } /> ) : (
Belum ada data pesanan
)}
)}
); }