dstpabuaran.com/resources/js/pages/admin/analysis/index.tsx
Yoga Pangestu c6a87a64c9 feat: enhance dashboard with detailed statistics and charts
- Refactored the dashboard component to include attendance, revenue, and expense summaries.
- Added donut charts for order statistics by channel, payment type, marketing, and status.
- Implemented a greeting message based on the current time and user information.
- Updated routing to use DashboardController for the dashboard view.
- Introduced a new AnalysisController for future analysis features.
2026-08-07 08:35:01 +07:00

839 lines
41 KiB
TypeScript

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/date-picker';
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<string, string> = {
total: '#60a5fa',
net: '#22c55e',
net_warehouse: '#10b981',
net_retail: '#06b6d4',
deduction: '#f97316',
};
const EXPENSE_COLORS: Record<string, string> = {
total: '#60a5fa',
purchase: '#f97316',
expense: '#a855f7',
advance: '#ef4444',
};
const CHANNEL_COLORS: Record<string, string> = {
store: '#22c55e',
shopee: '#ee4d2d',
tiktok: '#000000',
};
const PAYMENT_COLORS: Record<string, string> = {
cash: '#22c55e',
transfer: '#60a5fa',
qris: '#a855f7',
marketplace: '#f97316',
};
type TooltipProps = {
active?: boolean;
payload?: Array<{
name: string;
value: number;
color?: string;
payload: Record<string, unknown>;
}>;
label?: string;
};
function BarTooltip({ active, payload, label }: TooltipProps) {
if (!active || !payload?.length) {
return null;
}
return (
<div className="rounded-lg border bg-background px-3 py-1.5 shadow-xl">
<p className="font-medium">{label}</p>
{payload.map((item, i) => (
<p key={i} className="flex items-center gap-1 text-sm">
<span className="size-2 rounded-full" style={{ backgroundColor: item.color }} />
<span className="text-muted-foreground">{item.name}</span>
<span className="ml-auto font-medium tabular-nums">
{typeof item.value === 'number' && item.value > 1000
? `Rp${formatRupiah(item.value)}`
: item.value}
</span>
</p>
))}
</div>
);
}
function DonutTooltip({ active, payload }: TooltipProps) {
if (!active || !payload?.length) {
return null;
}
const data = payload[0].payload;
return (
<div className="rounded-lg border bg-background px-3 py-1.5 shadow-xl">
<p className="flex items-center gap-1 text-sm">
<span className="size-2 rounded-full" style={{ backgroundColor: data.color as string }} />
<span className="text-muted-foreground">{(data.label as string) ?? ''}</span>
<span className="ml-auto font-medium tabular-nums">
{data.total !== undefined ? `Rp${formatRupiah(data.total as number)}` : (data.value as number)?.toLocaleString('id-ID')}
</span>
</p>
</div>
);
}
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 (
<>
<Head title="Analisa" />
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<h2 className="text-2xl font-bold tracking-tight">Analisa</h2>
</div>
<div className="flex flex-wrap items-center gap-3">
<Select value={selectedPreset} onValueChange={onPresetChange}>
<SelectTrigger className="h-9 w-[140px]">
<SelectValue placeholder="Filter Cepat" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="today">Hari Ini</SelectItem>
<SelectItem value="week">Minggu Ini</SelectItem>
<SelectItem value="month">Bulan Ini</SelectItem>
<SelectItem value="year">Tahun Ini</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">Mulai:</span>
<DatePicker value={startDate} onChange={(d) => setStartDate(d ? formatDate(d) : '')} className="w-[170px]" placeholder="Pilih tanggal" />
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-semibold tracking-wider text-muted-foreground uppercase">Sampai:</span>
<DatePicker value={endDate} onChange={(d) => setEndDate(d ? formatDate(d) : '')} className="w-[170px]" placeholder="Pilih tanggal" />
</div>
{hasActiveFilters && (
<Button variant="ghost" size="sm" onClick={clearFilters} className="h-9 px-3">
Reset Filter
</Button>
)}
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4" style={{ order: sectionOrder.statCards ?? 99 }}>
{can('analysis.attendance') && isManager && (
<StatCard
title="Kehadiran"
icon={UserCheck}
mainLabel="Total Karyawan"
mainValue={attendance.total_employees}
subLabel={`${attendance.percentage}% hadir`}
items={[
{ label: 'Hadir', value: attendance.present },
{ label: 'Tidak Hadir', value: attendance.absent },
{ label: 'Cuti', value: attendance.on_leave },
]}
/>
)}
{can('analysis.attendance') && !isManager && myAttendance && (
<StatCard
title="Kehadiran Saya"
icon={UserCheck}
mainLabel="Hari Kerja"
mainValue={myAttendance.total_days}
subLabel={`${myAttendance.percentage}% hadir`}
items={[
{ label: 'Hadir', value: myAttendance.present_days },
{ label: 'Tidak Hadir', value: myAttendance.absent_days },
{ label: 'Cuti', value: myAttendance.leave_days },
]}
/>
)}
{can('analysis.cash') && (
<StatCard
title="Kas Toko"
icon={Banknote}
mainLabel="Total Saldo"
mainValue={`Rp${formatRupiah(cashOverview.total_balance)}`}
subLabel={`${cashOverview.total_transactions} transaksi`}
items={[
{ label: 'Deposit', value: `Rp${formatRupiah(cashOverview.total_deposit)}` },
{ label: 'Withdrawal', value: `Rp${formatRupiah(cashOverview.total_withdrawal)}` },
]}
cols={2}
/>
)}
{can('analysis.raw_materials') && (
<StatCard
title="Bahan Baku"
icon={Package}
mainLabel="Total Stok"
mainValue={rawMaterialStock.total_stock.toLocaleString('id-ID')}
subLabel={`Rp${formatRupiah(rawMaterialStock.total_value)}`}
items={[
{ label: 'Yard', value: rawMaterialStock.by_unit?.yard?.toLocaleString('id-ID') ?? '0' },
{ label: 'Meter', value: rawMaterialStock.by_unit?.meter?.toLocaleString('id-ID') ?? '0' },
{ label: 'Kg', value: rawMaterialStock.by_unit?.kilogram?.toLocaleString('id-ID') ?? '0' },
]}
/>
)}
{can('analysis.product_stock') && (
<StatCard
title="Stok Produk"
icon={ShoppingCart}
mainLabel="Total Stok"
mainValue={(productStock.total_stock + productStock.total_reject + productStock.total_retail).toLocaleString('id-ID')}
subLabel={`Rp${formatRupiah(productStock.total_value)}`}
items={[
{ label: 'Stok Bagus', value: productStock.total_stock.toLocaleString('id-ID') },
{ label: 'Stok Reject', value: productStock.total_reject.toLocaleString('id-ID') },
{ label: 'Stok Ecer', value: productStock.total_retail.toLocaleString('id-ID') },
]}
/>
)}
</div>
{can('analysis.revenue') && (
<Card className="py-4 sm:py-0" style={{ order: sectionOrder.revenue ?? 99 }}>
<CardHeader className="flex flex-col items-stretch border-b p-0! sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Pendapatan</CardTitle>
</div>
<div className="flex flex-col sm:flex-row">
{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]) => (
<div key={key} className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6">
<span className="text-xs text-muted-foreground">{key === 'net_warehouse' ? 'Total Gudang' : key === 'net_retail' ? 'Total Ecer' : key === 'total' ? 'Total' : key === 'net' ? 'Bersih' : 'Potongan'}</span>
<span className="text-sm">Rp{formatRupiah(key === 'total' ? revenueSummary.total_revenue : key === 'deduction' ? revenueSummary.total_deduction : 0)}</span>
</div>
))}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
{monthlyRevenue.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={monthlyRevenue}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `Rp${formatRupiahShort(v)}`} />
<Tooltip content={<BarTooltip />} />
<Bar dataKey="total" fill={REVENUE_COLORS.total} radius={[4, 4, 0, 0]} name="Total" />
<Bar dataKey="deduction" fill={REVENUE_COLORS.deduction} radius={[4, 4, 0, 0]} name="Potongan" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex h-[300px] items-center justify-center text-muted-foreground">Belum ada data pendapatan</div>
)}
</CardContent>
</Card>
)}
{can('analysis.revenue') && (
<div className="grid gap-4 md:grid-cols-2" style={{ order: sectionOrder.revenueByChannel ?? 99 }}>
<Card className="py-4 sm:py-0">
<CardHeader className="flex flex-col items-stretch border-b p-0! sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Pendapatan per Channel</CardTitle>
</div>
<div className="flex flex-col sm:flex-row">
{revenueByChannelData.map((item) => (
<div key={item.channel} className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-4">
<span className="text-xs text-muted-foreground">{item.label}</span>
<span className="text-sm">Rp{formatRupiah(item.total)}</span>
</div>
))}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
{revenueByChannelData.length > 0 ? (
<div className="mx-auto aspect-square max-h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie data={revenueByChannelData} dataKey="total" nameKey="label" cx="50%" cy="50%" outerRadius={100} innerRadius={60} strokeWidth={2} stroke="#374151">
{revenueByChannelData.map((entry, index) => (
<Cell key={index} fill={entry.color} />
))}
</Pie>
<Tooltip content={<DonutTooltip />} />
</PieChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex h-[300px] items-center justify-center text-muted-foreground">Belum ada data pendapatan</div>
)}
</CardContent>
</Card>
<Card className="py-4 sm:py-0">
<CardHeader className="flex flex-col items-stretch border-b p-0! sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Pendapatan per Pembayaran</CardTitle>
</div>
<div className="flex flex-col sm:flex-row">
{revenueByPaymentType.map((item) => (
<div key={item.payment_type} className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-4">
<span className="text-xs text-muted-foreground">{item.label}</span>
<span className="text-sm">Rp{formatRupiah(item.total)}</span>
</div>
))}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
{revenueByPaymentType.length > 0 ? (
<div className="mx-auto aspect-square max-h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={revenueByPaymentType.map((item) => ({
...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) => (
<Cell key={index} fill={PAYMENT_COLORS[entry.payment_type] ?? '#94a3b8'} />
))}
</Pie>
<Tooltip content={<DonutTooltip />} />
</PieChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex h-[300px] items-center justify-center text-muted-foreground">Belum ada data pendapatan</div>
)}
</CardContent>
</Card>
</div>
)}
{can('analysis.expense') && (
<Card className="py-4 sm:py-0" style={{ order: sectionOrder.expense ?? 99 }}>
<CardHeader className="flex flex-col items-stretch border-b p-0! sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Pengeluaran</CardTitle>
</div>
<div className="flex flex-col sm:flex-row">
{visibleExpenseCharts.map((chart) => (
<div key={chart} className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6">
<span className="text-xs text-muted-foreground">{chart === 'total' ? 'Total' : chart === 'purchase' ? 'Belanja' : chart === 'expense' ? 'Pengeluaran' : 'Kasbon'}</span>
<span className="text-sm">Rp{formatRupiah(chart === 'total' ? expenseSummary.total : chart === 'purchase' ? expenseSummary.purchase_total : chart === 'expense' ? expenseSummary.expense_total : expenseSummary.advance_total)}</span>
</div>
))}
</div>
</CardHeader>
<CardContent className="px-2 sm:p-6">
{monthlyExpense.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={monthlyExpense}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `Rp${formatRupiahShort(v)}`} />
<Tooltip content={<BarTooltip />} />
<Bar dataKey="total" fill={EXPENSE_COLORS.total} radius={[4, 4, 0, 0]} name="Total" />
<Bar dataKey="expense" fill={EXPENSE_COLORS.expense} radius={[4, 4, 0, 0]} name="Pengeluaran" />
<Bar dataKey="advance" fill={EXPENSE_COLORS.advance} radius={[4, 4, 0, 0]} name="Kasbon" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex h-[300px] items-center justify-center text-muted-foreground">Belum ada data pengeluaran</div>
)}
</CardContent>
</Card>
)}
{(can('analysis.profit_gross') || can('analysis.profit_hpp')) && (
<div style={{ order: sectionOrder.profitGross ?? 99 }}>
<StatCard
title="Laba Kotor"
icon={TrendingUp}
mainLabel="Laba Kotor"
mainValue={`Rp${formatRupiah(profitMetrics.gross_profit)}`}
items={[
{ label: 'Pendapatan', value: `Rp${formatRupiah(revenueSummary.total_revenue)}` },
{ label: 'HPP', value: `Rp${formatRupiah(profitMetrics.hpp)}` },
...(!hasRole('cashier')
? [{ label: 'Laba Bersih', value: `Rp${formatRupiah(profitMetrics.net_profit)} (${profitMetrics.profit_margin}%)` }]
: []),
]}
/>
</div>
)}
{can('analysis.profit_orders') && (
<div style={{ order: sectionOrder.totalOrder ?? 99 }}>
<StatCard
title="Total Order"
icon={ShoppingCart}
mainLabel="Pesanan Selesai"
mainValue={profitMetrics.total_orders.toLocaleString('id-ID')}
items={[
{ label: 'Produk Terjual', value: profitMetrics.total_products_sold.toLocaleString('id-ID') },
{ label: 'Item/Transaksi', value: profitMetrics.items_per_transaction },
{ label: 'Rata-rata/Transaksi', value: `Rp${formatRupiah(profitMetrics.aov)}` },
]}
/>
</div>
)}
{can('analysis.marketing_sales') && (
<Card style={{ order: sectionOrder.marketingSales ?? 99 }}>
<CardHeader>
<CardTitle>Penjualan Marketing</CardTitle>
<CardDescription>Rekap penjualan per marketing</CardDescription>
</CardHeader>
<CardContent>
{marketingSales.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Marketing</th>
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Total Order</th>
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Produk Terjual</th>
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Total Pendapatan</th>
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Total Subtotal</th>
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Total Diskon</th>
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Rata-rata Order</th>
</tr>
</thead>
<tbody>
{marketingSales.map((item, index) => (
<tr key={index} className="border-b last:border-0">
<td className="px-4 py-3 font-medium">{item.marketing_name}</td>
<td className="px-4 py-3 text-right tabular-nums">{item.total_orders.toLocaleString('id-ID')}</td>
<td className="px-4 py-3 text-right tabular-nums">{item.total_products_sold.toLocaleString('id-ID')} pcs</td>
<td className="px-4 py-3 text-right tabular-nums">Rp{formatRupiah(item.total_revenue)}</td>
<td className="px-4 py-3 text-right tabular-nums">Rp{formatRupiah(item.total_subtotal)}</td>
<td className="px-4 py-3 text-right tabular-nums">Rp{formatRupiah(item.total_discount)}</td>
<td className="px-4 py-3 text-right tabular-nums">Rp{formatRupiah(item.avg_order)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="flex h-[150px] items-center justify-center text-muted-foreground">Belum ada data penjualan marketing</div>
)}
</CardContent>
</Card>
)}
{can('analysis.top_suppliers') && (
<Card style={{ order: sectionOrder.topSuppliers ?? 99 }}>
<CardHeader>
<CardTitle className="text-base">Top 5 Supplier</CardTitle>
<CardDescription>Berdasarkan total harga pembelian</CardDescription>
</CardHeader>
<CardContent>
{topSuppliers.length > 0 ? (
<ResponsiveContainer width="100%" height={250}>
<BarChart data={topSuppliers.map((s) => ({ name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name, amount: s.total_amount }))}>
<CartesianGrid vertical={false} />
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `Rp${formatRupiahShort(v)}`} />
<Tooltip content={<BarTooltip />} />
<Bar dataKey="amount" fill="#60a5fa" radius={[4, 4, 0, 0]} name="Total Pembelian" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex h-[250px] items-center justify-center text-muted-foreground">Belum ada data supplier</div>
)}
</CardContent>
</Card>
)}
{can('analysis.top_products') && (
<Card style={{ order: sectionOrder.topProducts ?? 99 }}>
<CardHeader>
<CardTitle className="text-base">Top 5 Produk</CardTitle>
<CardDescription>Berdasarkan jumlah terjual</CardDescription>
</CardHeader>
<CardContent>
{topProducts.length > 0 ? (
<ResponsiveContainer width="100%" height={250}>
<BarChart data={topProducts.map((p) => ({ name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name, qty: p.total_qty }))}>
<CartesianGrid vertical={false} />
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis tickLine={false} axisLine={false} />
<Tooltip content={<BarTooltip />} />
<Bar dataKey="qty" fill="#a855f7" radius={[4, 4, 0, 0]} name="Jumlah Terjual" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex h-[250px] items-center justify-center text-muted-foreground">Belum ada data produk</div>
)}
</CardContent>
</Card>
)}
{can('analysis.top_customers') && (
<Card style={{ order: sectionOrder.topCustomers ?? 99 }}>
<CardHeader>
<CardTitle className="text-base">Top 5 Pelanggan</CardTitle>
<CardDescription>Berdasarkan total nilai pesanan</CardDescription>
</CardHeader>
<CardContent>
{topCustomers.length > 0 ? (
<ResponsiveContainer width="100%" height={250}>
<BarChart data={topCustomers.map((c) => ({ name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name, amount: c.total_amount }))}>
<CartesianGrid vertical={false} />
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `Rp${formatRupiahShort(v)}`} />
<Tooltip content={<BarTooltip />} />
<Bar dataKey="amount" fill="#22c55e" radius={[4, 4, 0, 0]} name="Total Pesanan" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex h-[250px] items-center justify-center text-muted-foreground">Belum ada data pelanggan</div>
)}
</CardContent>
</Card>
)}
{can('analysis.busy_hours') && (
<Card style={{ order: sectionOrder.busyHours ?? 99 }}>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Jam Sibuk Toko</CardTitle>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground">Jam Tersibuk</p>
<p className="text-2xl font-bold text-primary">{peakHour.hour}</p>
<p className="text-xs text-muted-foreground">{peakHour.orders} pesanan</p>
</div>
</div>
</CardHeader>
<CardContent>
{busyHours.length > 0 ? (
<ResponsiveContainer width="100%" height={250}>
<BarChart data={busyHours}>
<CartesianGrid vertical={false} />
<XAxis dataKey="hour" tickLine={false} axisLine={false} tickMargin={8} />
<YAxis tickLine={false} axisLine={false} tickFormatter={(v) => Math.round(v).toString()} />
<Tooltip content={<BarTooltip />} />
<Bar dataKey="orders" fill="#60a5fa" radius={[4, 4, 0, 0]} name="Pesanan" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex h-[250px] items-center justify-center text-muted-foreground">Belum ada data pesanan</div>
)}
</CardContent>
</Card>
)}
</div>
</>
);
}