store/resources/js/pages/admin/Dashboard.vue

1104 lines
52 KiB
Vue

<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import {
AlertTriangle,
Banknote,
Clock,
CreditCard,
DollarSign,
FileText,
Moon,
Package,
ShoppingCart,
Sun,
Sunrise,
TrendingDown,
TrendingUp,
UserCheck,
UserX,
Users,
} from '@lucide/vue';
import { VisGroupedBar, VisXYContainer, VisDonut } from '@unovis/vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { ChartContainer } from '@/components/ui/chart';
import type { ChartConfig } from '@/components/ui/chart';
import AdminLayout from '@/layouts/AdminLayout.vue';
interface DashboardProps {
rawMaterialStock: {
total_stock: number;
total_value: number;
by_unit: Record<string, number>;
};
productStock: {
total_stock: number;
total_reject: number;
total_value: number;
total_variants: number;
total_products: number;
total_categories: number;
};
lowStockProducts: Array<{
name: string;
stock: number;
}>;
lowStockMaterials: Array<{
name: string;
stock: number;
unit: string;
}>;
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;
}>;
purchaseSummary: {
total_purchases: number;
total_spent: number;
total_discount: number;
};
cuttingSummary: {
total_cost: number;
total_cutting: number;
total_warehouse: number;
total_reject: number;
};
cuttingByStatus: Array<{
status: string;
label: string;
count: 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;
}>;
};
revenueSummary: {
total_revenue: number;
total_discount: number;
total_marketplace_fees: number;
total_potongan: number;
total_shipping: number;
total_orders: number;
avg_order: number;
};
marketplaceSummary: {
total_revenue: number;
total_fees: number;
total_net: number;
total_orders: number;
};
cashAccounts: Array<{
name: string;
balance: number;
}>;
monthlyCashFlow: Array<{
label: string;
deposits: number;
withdrawals: number;
}>;
monthlyExpenses: {
total: number;
count: number;
};
kasbonSummary: {
pending: { count: number; total: number };
approved: { count: number; total: number };
paid: { count: number; total: number };
total: number;
};
payrollSummary: {
has_period: boolean;
period_status?: string;
total_employees: number;
total_amount: number;
paid_amount: number;
unpaid_amount: number;
paid_count: number;
unpaid_count: number;
};
leaveRequestSummary: {
total: number;
pending: number;
approved: number;
rejected: number;
};
employeeSummary: {
total: number;
by_status: Array<{
status: string;
label: string;
count: number;
}>;
};
attendanceToday: {
total_employees: number;
present: number;
absent: number;
};
monthlyRevenueTrend: Array<{
label: string;
total: number;
count: number;
}>;
monthlyPurchaseTrend: Array<{
label: string;
total: number;
count: number;
}>;
}
const props = defineProps<DashboardProps>();
const currentTime = ref(new Date());
let timer: ReturnType<typeof setInterval>;
onMounted(() => {
timer = setInterval(() => {
currentTime.value = new Date();
}, 1000);
});
onUnmounted(() => {
clearInterval(timer);
});
const timeStr = computed(() => {
return currentTime.value.toLocaleTimeString('id-ID', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
});
const dateStr = computed(() => {
return currentTime.value.toLocaleDateString('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
});
const greeting = computed(() => {
const hour = currentTime.value.getHours();
if (hour >= 4 && hour < 11) {
return { text: 'Selamat Pagi', icon: Sunrise };
}
if (hour >= 11 && hour < 15) {
return { text: 'Selamat Siang', icon: Sun };
}
if (hour >= 15 && hour < 18) {
return { text: 'Selamat Sore', icon: Sun };
}
return { text: 'Selamat Malam', icon: Moon };
});
function formatRupiah(value: number): string {
return 'Rp ' + value.toLocaleString('id-ID');
}
// Chart configs
const supplierChartConfig = {
amount: {
label: 'Total Pembelian',
color: 'var(--chart-1)',
},
} satisfies ChartConfig;
const customerChartConfig = {
amount: {
label: 'Total Pesanan',
color: 'var(--chart-2)',
},
} satisfies ChartConfig;
const productChartConfig = {
qty: {
label: 'Jumlah Terjual',
color: 'var(--chart-3)',
},
} satisfies ChartConfig;
const channelColors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)'];
const channelChartConfig = computed(() => {
const config: ChartConfig = {};
props.orderStats.by_channel.forEach((item, index) => {
config[item.channel] = {
label: item.label,
color: channelColors[index % channelColors.length],
};
});
return config;
});
const paymentColors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)'];
const paymentChartConfig = computed(() => {
const config: ChartConfig = {};
props.orderStats.by_payment_type.forEach((item, index) => {
config[item.payment_type] = {
label: item.label,
color: paymentColors[index % paymentColors.length],
};
});
return config;
});
const statusColors = ['#facc15', 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-4)'];
const statusChartConfig = computed(() => {
const config: ChartConfig = {};
props.orderStats.by_status.forEach((item, index) => {
config[item.status] = {
label: item.label,
color: statusColors[index % statusColors.length],
};
});
return config;
});
const marketingColors = ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'];
const marketingChartConfig = computed(() => {
const config: ChartConfig = {};
props.orderStats.by_marketing.forEach((item, index) => {
config[`mkt_${index}`] = {
label: item.name,
color: marketingColors[index % marketingColors.length],
};
});
return config;
});
type SupplierData = { name: string; amount: number };
type CustomerData = { name: string; amount: number };
type ProductData = { name: string; qty: number };
const supplierBarData = computed<SupplierData[]>(() => {
return props.topSuppliers.map((s) => ({
name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name,
amount: s.total_amount,
}));
});
const customerBarData = computed<CustomerData[]>(() => {
return props.topCustomers.map((c) => ({
name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name,
amount: c.total_amount,
}));
});
const productBarData = computed<ProductData[]>(() => {
return props.topProducts.map((p) => ({
name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name,
qty: p.total_qty,
}));
});
const totalCashBalance = computed(() => {
return props.cashAccounts.reduce((sum, acc) => sum + acc.balance, 0);
});
</script>
<template>
<Head title="Dashboard" />
<AdminLayout>
<div class="flex flex-1 flex-col gap-6">
<!-- Welcome Card -->
<Card class="relative overflow-hidden">
<div class="from-primary/5 to-background absolute inset-0 bg-linear-to-br" />
<CardHeader class="relative">
<div class="flex items-center gap-3">
<div class="bg-primary/10 flex size-12 items-center justify-center rounded-full">
<component :is="greeting.icon" class="text-primary size-6" />
</div>
<div>
<CardTitle class="text-2xl font-bold">
{{ greeting.text }}, {{ $page.props.auth.user?.username }}!
</CardTitle>
<CardDescription class="mt-1">
Selamat datang di dasbor aplikasi.
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent class="relative">
<div class="text-muted-foreground flex items-center gap-2 text-sm">
<span>{{ dateStr }}</span>
<span>-</span>
<span class="flex items-center gap-1">
<Clock class="size-3.5" />
{{ timeStr }}
</span>
</div>
</CardContent>
</Card>
<!-- Row 1: Stock & Finance Summary -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- 1. Raw Material Stock -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Bahan Baku</CardTitle>
<Package class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-3">
<div>
<p class="text-muted-foreground text-xs">Total Stok</p>
<p class="text-xl font-bold">{{ rawMaterialStock.total_stock.toLocaleString('id-ID') }}
</p>
<p class="text-muted-foreground text-xs">
{{ formatRupiah(rawMaterialStock.total_value) }}
</p>
</div>
<div class="grid grid-cols-3 gap-2 pt-2 border-t text-center">
<div>
<Badge variant="outline"
class="mb-1 bg-blue-500/10 text-blue-600 border-blue-500/20">
Yard
</Badge>
<p class="text-sm font-semibold">{{ (rawMaterialStock.by_unit?.yard ??
0).toLocaleString('id-ID') }}</p>
</div>
<div>
<Badge variant="outline"
class="mb-1 bg-green-500/10 text-green-600 border-green-500/20">
Meter
</Badge>
<p class="text-sm font-semibold">{{ (rawMaterialStock.by_unit?.meter ??
0).toLocaleString('id-ID') }}</p>
</div>
<div>
<Badge variant="outline"
class="mb-1 bg-purple-500/10 text-purple-600 border-purple-500/20">
Kg
</Badge>
<p class="text-sm font-semibold">{{ (rawMaterialStock.by_unit?.kilogram ??
0).toLocaleString('id-ID') }}</p>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- 2. Product Stock -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Stok Produk</CardTitle>
<ShoppingCart class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-3">
<div>
<p class="text-muted-foreground text-xs">Total Stok</p>
<p class="text-xl font-bold">{{ productStock.total_stock.toLocaleString('id-ID') }}</p>
<p class="text-muted-foreground text-xs">
{{ formatRupiah(productStock.total_value) }}
<span v-if="productStock.total_reject > 0" class="ml-1 text-red-500">
({{ productStock.total_reject }} reject)
</span>
</p>
</div>
<div class="grid grid-cols-3 gap-2 pt-2 border-t text-center">
<div>
<Badge variant="outline"
class="mb-1 bg-green-500/10 text-green-600 border-green-500/20">
Produk
</Badge>
<p class="text-sm font-semibold">{{
productStock.total_products.toLocaleString('id-ID') }}</p>
</div>
<div>
<Badge variant="outline"
class="mb-1 bg-blue-500/10 text-blue-600 border-blue-500/20">
Variant
</Badge>
<p class="text-sm font-semibold">{{
productStock.total_variants.toLocaleString('id-ID') }}</p>
</div>
<div>
<Badge variant="outline"
class="mb-1 bg-purple-500/10 text-purple-600 border-purple-500/20">
Kategori
</Badge>
<p class="text-sm font-semibold">{{
productStock.total_categories.toLocaleString('id-ID') }}</p>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Total Pendapatan -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Total Pendapatan</CardTitle>
<TrendingUp class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-3">
<div>
<p class="text-muted-foreground text-xs">Total</p>
<p class="text-xl font-bold text-green-600">{{
formatRupiah(revenueSummary.total_revenue) }}</p>
<p class="text-muted-foreground text-xs">
{{ revenueSummary.total_orders }} pesanan selesai
</p>
</div>
<div class="grid grid-cols-3 gap-2 pt-2 border-t text-center">
<div v-for="(item, index) in orderStats.by_channel" :key="item.channel">
<Badge variant="outline" :class="[
'mb-1',
index === 0 ? 'bg-blue-500/10 text-blue-600 border-blue-500/20' :
index === 1 ? 'bg-orange-500/10 text-orange-600 border-orange-500/20' :
'bg-purple-500/10 text-purple-600 border-purple-500/20'
]">
{{ item.label }}
</Badge>
<p class="text-sm font-semibold">{{ formatRupiah(item.total) }}</p>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Total Pengeluaran -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Total Pengeluaran</CardTitle>
<TrendingDown class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-3">
<div>
<p class="text-muted-foreground text-xs">Total</p>
<p class="text-xl font-bold text-red-600">
{{ formatRupiah(purchaseSummary.total_spent + monthlyExpenses.total +
kasbonSummary.total) }}
</p>
</div>
<div class="grid grid-cols-3 gap-2 pt-2 border-t text-center">
<div>
<Badge variant="outline"
class="mb-1 bg-blue-500/10 text-blue-600 border-blue-500/20">
Belanja
</Badge>
<p class="text-sm font-semibold">{{ formatRupiah(purchaseSummary.total_spent) }}</p>
</div>
<div>
<Badge variant="outline"
class="mb-1 bg-orange-500/10 text-orange-600 border-orange-500/20">
Pengeluaran
</Badge>
<p class="text-sm font-semibold">{{ formatRupiah(monthlyExpenses.total) }}</p>
</div>
<div>
<Badge variant="outline"
class="mb-1 bg-purple-500/10 text-purple-600 border-purple-500/20">
Kasbon
</Badge>
<p class="text-sm font-semibold">{{ formatRupiah(kasbonSummary.total) }}</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- Row 7: HR Summary -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- Attendance Today -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Absensi Hari Ini</CardTitle>
<UserCheck class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-3">
<div>
<p class="text-muted-foreground text-xs">Total Karyawan</p>
<p class="text-xl font-bold">{{ attendanceToday.total_employees }}</p>
</div>
<div class="grid grid-cols-2 gap-2 pt-2 border-t text-center">
<div class="flex items-center gap-2">
<UserCheck class="size-4 text-green-500" />
<div>
<p class="text-xs text-muted-foreground">Hadir</p>
<p class="text-lg font-semibold text-green-600">{{ attendanceToday.present }}
</p>
</div>
</div>
<div class="flex items-center gap-2">
<UserX class="size-4 text-red-500" />
<div>
<p class="text-xs text-muted-foreground">Tidak Hadir</p>
<p class="text-lg font-semibold text-red-600">{{ attendanceToday.absent }}</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Kasbon Summary -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Kasbon Karyawan</CardTitle>
<CreditCard class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-3">
<div>
<p class="text-muted-foreground text-xs">Total Keseluruhan</p>
<p class="text-xl font-bold">{{ formatRupiah(kasbonSummary.total) }}</p>
</div>
<div class="grid grid-cols-3 gap-2 pt-2 border-t text-center">
<div>
<Badge variant="outline"
class="mb-1 bg-yellow-500/10 text-yellow-600 border-yellow-500/20">
Menunggu
</Badge>
<p class="text-sm font-semibold">{{ kasbonSummary.pending.count }}</p>
<p class="text-muted-foreground text-xs">{{
formatRupiah(kasbonSummary.pending.total) }}</p>
</div>
<div>
<Badge variant="outline"
class="mb-1 bg-blue-500/10 text-blue-600 border-blue-500/20">
Disetujui
</Badge>
<p class="text-sm font-semibold">{{ kasbonSummary.approved.count }}</p>
<p class="text-muted-foreground text-xs">{{
formatRupiah(kasbonSummary.approved.total) }}</p>
</div>
<div>
<Badge variant="outline"
class="mb-1 bg-green-500/10 text-green-600 border-green-500/20">
Dibayar
</Badge>
<p class="text-sm font-semibold">{{ kasbonSummary.paid.count }}</p>
<p class="text-muted-foreground text-xs">{{ formatRupiah(kasbonSummary.paid.total)
}}</p>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Payroll Summary -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Penggajian Bulan Ini</CardTitle>
<Users class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div v-if="payrollSummary.has_period" class="space-y-3">
<div>
<p class="text-muted-foreground text-xs">Total Penggajian</p>
<p class="text-xl font-bold">{{ formatRupiah(payrollSummary.total_amount) }}</p>
<p class="text-muted-foreground text-xs">{{ payrollSummary.total_employees }} karyawan
</p>
</div>
<div class="grid grid-cols-2 gap-2 pt-2 border-t text-center">
<div>
<Badge variant="outline"
class="mb-1 bg-green-500/10 text-green-600 border-green-500/20">
Sudah Dibayar
</Badge>
<p class="text-sm font-semibold">{{ payrollSummary.paid_count }}</p>
<p class="text-muted-foreground text-xs">{{ formatRupiah(payrollSummary.paid_amount)
}}</p>
</div>
<div>
<Badge variant="outline" class="mb-1 bg-red-500/10 text-red-600 border-red-500/20">
Belum Dibayar
</Badge>
<p class="text-sm font-semibold">{{ payrollSummary.unpaid_count }}</p>
<p class="text-muted-foreground text-xs">{{
formatRupiah(payrollSummary.unpaid_amount) }}</p>
</div>
</div>
</div>
<div v-else class="text-muted-foreground flex h-[120px] items-center justify-center">
Belum ada periode payroll bulan ini
</div>
</CardContent>
</Card>
<!-- Leave Request Summary -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Pengajuan Cuti Bulan Ini</CardTitle>
<FileText class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-3">
<div>
<p class="text-muted-foreground text-xs">Total Pengajuan</p>
<p class="text-xl font-bold">{{ leaveRequestSummary.total }}</p>
</div>
<div class="grid grid-cols-3 gap-2 pt-2 border-t text-center">
<div>
<Badge variant="outline"
class="mb-1 bg-yellow-500/10 text-yellow-600 border-yellow-500/20">
Menunggu
</Badge>
<p class="text-lg font-semibold">{{ leaveRequestSummary.pending }}</p>
</div>
<div>
<Badge variant="outline"
class="mb-1 bg-green-500/10 text-green-600 border-green-500/20">
Disetujui
</Badge>
<p class="text-lg font-semibold">{{ leaveRequestSummary.approved }}</p>
</div>
<div>
<Badge variant="outline" class="mb-1 bg-red-500/10 text-red-600 border-red-500/20">
Ditolak
</Badge>
<p class="text-lg font-semibold">{{ leaveRequestSummary.rejected }}</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- Row 6: Revenue Detail, Cash, Expenses -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- Revenue Detail -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Detail Pendapatan</CardTitle>
<DollarSign class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-2">
<div>
<p class="text-muted-foreground text-xs">Rata-rata per Pesanan</p>
<p class="text-lg font-bold">{{ formatRupiah(revenueSummary.avg_order) }}</p>
</div>
<div class="grid grid-cols-2 gap-2 pt-2 border-t text-center">
<div>
<p class="text-muted-foreground text-xs">Total Diskon</p>
<p class="text-sm font-semibold text-orange-500">{{
formatRupiah(revenueSummary.total_discount) }}</p>
</div>
<div>
<p class="text-muted-foreground text-xs">Total Ongkir</p>
<p class="text-sm font-semibold">{{ formatRupiah(revenueSummary.total_shipping) }}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Cash Accounts -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Kas Toko</CardTitle>
<Banknote class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-2">
<div>
<p class="text-muted-foreground text-xs">Total Saldo</p>
<p class="text-xl font-bold">{{ formatRupiah(totalCashBalance) }}</p>
</div>
<div class="space-y-1 pt-2 border-t">
<div v-for="account in cashAccounts" :key="account.name"
class="flex items-center justify-between">
<span class="text-muted-foreground text-sm">{{ account.name }}</span>
<span class="text-sm font-medium">{{ formatRupiah(account.balance) }}</span>
</div>
<div v-if="cashAccounts.length === 0" class="text-muted-foreground text-sm">
Belum ada akun kas
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Rincian Marketplace -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Rincian Marketplace</CardTitle>
<ShoppingCart class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="space-y-3">
<div>
<p class="text-muted-foreground text-xs">Total Pendapatan</p>
<p class="text-xl font-bold text-green-600">{{ formatRupiah(marketplaceSummary.total_revenue) }}</p>
<p class="text-muted-foreground text-xs">
{{ marketplaceSummary.total_orders }} pesanan selesai
</p>
</div>
<div class="grid grid-cols-2 gap-2 pt-2 border-t text-center">
<div>
<Badge variant="outline" class="mb-1 bg-green-500/10 text-green-600 border-green-500/20">
Bersih
</Badge>
<p class="text-sm font-semibold">{{ formatRupiah(marketplaceSummary.total_net) }}</p>
</div>
<div>
<Badge variant="outline" class="mb-1 bg-red-500/10 text-red-600 border-red-500/20">
Potongan
</Badge>
<p class="text-sm font-semibold">{{ formatRupiah(marketplaceSummary.total_fees) }}</p>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Employee Summary -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">Karyawan</CardTitle>
<Users class="text-muted-foreground size-4" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">{{ employeeSummary.total }}</div>
<div class="mt-2 space-y-1">
<div v-for="item in employeeSummary.by_status" :key="item.status"
class="flex items-center justify-between text-xs">
<span class="text-muted-foreground">{{ item.label }}</span>
<span class="font-medium">{{ item.count }}</span>
</div>
</div>
</CardContent>
</Card>
</div>
<!-- Row 2: Low Stock Alerts -->
<div class="grid gap-4 md:grid-cols-2">
<!-- Low Stock Products -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<div>
<CardTitle class="text-sm font-medium">Stok Produk Menipis</CardTitle>
<CardDescription>Stok <= {{ 5 }} pcs</CardDescription>
</div>
<AlertTriangle class="size-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div v-if="lowStockProducts.length > 0" class="space-y-2">
<div v-for="item in lowStockProducts" :key="item.name"
class="flex items-center justify-between rounded-md border px-3 py-2">
<span class="text-sm">{{ item.name }}</span>
<Badge variant="outline" class="bg-yellow-500/10 text-yellow-600 border-yellow-500/20">
{{ item.stock }} pcs
</Badge>
</div>
</div>
<div v-else class="text-muted-foreground flex h-[80px] items-center justify-center text-sm">
Semua stok produk aman
</div>
</CardContent>
</Card>
<!-- Low Stock Materials -->
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<div>
<CardTitle class="text-sm font-medium">Stok Bahan Baku Menipis</CardTitle>
<CardDescription>Stok <= 5</CardDescription>
</div>
<AlertTriangle class="size-4 text-orange-500" />
</CardHeader>
<CardContent>
<div v-if="lowStockMaterials.length > 0" class="space-y-2">
<div v-for="item in lowStockMaterials" :key="item.name"
class="flex items-center justify-between rounded-md border px-3 py-2">
<span class="text-sm">{{ item.name }}</span>
<Badge variant="outline" class="bg-orange-500/10 text-orange-600 border-orange-500/20">
{{ item.stock }} {{ item.unit }}
</Badge>
</div>
</div>
<div v-else class="text-muted-foreground flex h-[80px] items-center justify-center text-sm">
Semua stok bahan baku aman
</div>
</CardContent>
</Card>
</div>
<!-- Row 3: Top 5 Charts -->
<div class="grid gap-4 md:grid-cols-3">
<!-- Top 5 Suppliers Chart -->
<Card>
<CardHeader>
<CardTitle class="text-base">Top 5 Supplier</CardTitle>
<CardDescription>Berdasarkan total harga pembelian</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer v-if="supplierBarData.length > 0" :config="supplierChartConfig"
class="min-h-[250px] w-full">
<VisXYContainer :data="supplierBarData">
<VisGroupedBar :x="(d: SupplierData) => d.name" :y="(d: SupplierData) => d.amount"
:color="supplierChartConfig.amount.color" :rounded-corners="4" bar-padding="0.1" />
</VisXYContainer>
</ChartContainer>
<div v-else class="text-muted-foreground flex h-[250px] items-center justify-center">
Belum ada data supplier
</div>
</CardContent>
</Card>
<!-- Top 5 Customers Chart -->
<Card>
<CardHeader>
<CardTitle class="text-base">Top 5 Pelanggan</CardTitle>
<CardDescription>Berdasarkan total nilai pesanan</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer v-if="customerBarData.length > 0" :config="customerChartConfig"
class="min-h-[250px] w-full">
<VisXYContainer :data="customerBarData">
<VisGroupedBar :x="(d: CustomerData) => d.name" :y="(d: CustomerData) => d.amount"
:color="customerChartConfig.amount.color" :rounded-corners="4" bar-padding="0.1" />
</VisXYContainer>
</ChartContainer>
<div v-else class="text-muted-foreground flex h-[250px] items-center justify-center">
Belum ada data pelanggan
</div>
</CardContent>
</Card>
<!-- Top 5 Products Chart -->
<Card>
<CardHeader>
<CardTitle class="text-base">Top 5 Produk</CardTitle>
<CardDescription>Berdasarkan jumlah terjual</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer v-if="productBarData.length > 0" :config="productChartConfig"
class="min-h-[250px] w-full">
<VisXYContainer :data="productBarData">
<VisGroupedBar :x="(d: ProductData) => d.name" :y="(d: ProductData) => d.qty"
:color="productChartConfig.qty.color" :rounded-corners="4" bar-padding="0.1" />
</VisXYContainer>
</ChartContainer>
<div v-else class="text-muted-foreground flex h-[250px] items-center justify-center">
Belum ada data produk
</div>
</CardContent>
</Card>
</div>
<!-- Row 4: Order Stats Charts -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- Order by Channel -->
<Card>
<CardHeader>
<CardTitle class="text-base">Pesanan per Channel</CardTitle>
</CardHeader>
<CardContent>
<div v-if="orderStats.by_channel.length > 0">
<ChartContainer :config="channelChartConfig" class="min-h-[200px] w-full">
<VisXYContainer :data="orderStats.by_channel">
<VisDonut :value="(d: typeof orderStats.by_channel[number]) => d.count"
:color="orderStats.by_channel.map((_, i) => channelColors[i % channelColors.length])" />
</VisXYContainer>
</ChartContainer>
<div class="mt-3 flex flex-wrap justify-center gap-3">
<div v-for="(item, index) in orderStats.by_channel" :key="item.channel"
class="flex items-center gap-1.5">
<span class="size-2.5 rounded-full"
:style="{ backgroundColor: channelColors[index % channelColors.length] }" />
<span class="text-muted-foreground text-xs">{{ item.label }}</span>
<span class="text-xs font-medium">{{ item.count }}</span>
</div>
</div>
</div>
<div v-else class="text-muted-foreground flex h-[200px] items-center justify-center">
Belum ada data
</div>
</CardContent>
</Card>
<!-- Order by Payment Type -->
<Card>
<CardHeader>
<CardTitle class="text-base">Pesanan per Pembayaran</CardTitle>
</CardHeader>
<CardContent>
<div v-if="orderStats.by_payment_type.length > 0">
<ChartContainer :config="paymentChartConfig" class="min-h-[200px] w-full">
<VisXYContainer :data="orderStats.by_payment_type">
<VisDonut :value="(d: typeof orderStats.by_payment_type[number]) => d.count"
:color="orderStats.by_payment_type.map((_, i) => paymentColors[i % paymentColors.length])" />
</VisXYContainer>
</ChartContainer>
<div class="mt-3 flex flex-wrap justify-center gap-3">
<div v-for="(item, index) in orderStats.by_payment_type" :key="item.payment_type"
class="flex items-center gap-1.5">
<span class="size-2.5 rounded-full"
:style="{ backgroundColor: paymentColors[index % paymentColors.length] }" />
<span class="text-muted-foreground text-xs">{{ item.label }}</span>
<span class="text-xs font-medium">{{ item.count }}</span>
</div>
</div>
</div>
<div v-else class="text-muted-foreground flex h-[200px] items-center justify-center">
Belum ada data
</div>
</CardContent>
</Card>
<!-- Order by Marketing -->
<Card>
<CardHeader>
<CardTitle class="text-base">Pesanan per Marketing</CardTitle>
</CardHeader>
<CardContent>
<div v-if="orderStats.by_marketing.length > 0">
<ChartContainer :config="marketingChartConfig" class="min-h-[200px] w-full">
<VisXYContainer :data="orderStats.by_marketing">
<VisDonut :value="(d: typeof orderStats.by_marketing[number]) => d.count"
:color="orderStats.by_marketing.map((_, i) => marketingColors[i % marketingColors.length])" />
</VisXYContainer>
</ChartContainer>
<div class="mt-3 flex flex-wrap justify-center gap-3">
<div v-for="(item, index) in orderStats.by_marketing" :key="item.name"
class="flex items-center gap-1.5">
<span class="size-2.5 rounded-full"
:style="{ backgroundColor: marketingColors[index % marketingColors.length] }" />
<span class="text-muted-foreground text-xs">{{ item.name }}</span>
<span class="text-xs font-medium">{{ item.count }}</span>
</div>
</div>
</div>
<div v-else class="text-muted-foreground flex h-[200px] items-center justify-center">
Belum ada data
</div>
</CardContent>
</Card>
<!-- Order by Status -->
<Card>
<CardHeader>
<CardTitle class="text-base">Pesanan per Status</CardTitle>
</CardHeader>
<CardContent>
<div v-if="orderStats.by_status.length > 0">
<ChartContainer :config="statusChartConfig" class="min-h-[200px] w-full">
<VisXYContainer :data="orderStats.by_status">
<VisDonut :value="(d: typeof orderStats.by_status[number]) => d.count"
:color="orderStats.by_status.map((_, i) => statusColors[i % statusColors.length])" />
</VisXYContainer>
</ChartContainer>
<div class="mt-3 flex flex-wrap justify-center gap-3">
<div v-for="(item, index) in orderStats.by_status" :key="item.status"
class="flex items-center gap-1.5">
<span class="size-2.5 rounded-full"
:style="{ backgroundColor: statusColors[index % statusColors.length] }" />
<span class="text-muted-foreground text-xs">{{ item.label }}</span>
<span class="text-xs font-medium">{{ item.count }}</span>
</div>
</div>
</div>
<div v-else class="text-muted-foreground flex h-[200px] items-center justify-center">
Belum ada data
</div>
</CardContent>
</Card>
</div>
<!-- Row 8: Monthly Trends -->
<div class="grid gap-4 md:grid-cols-2">
<!-- Monthly Revenue Trend -->
<Card>
<CardHeader>
<CardTitle class="text-base">Tren Pendapatan 6 Bulan</CardTitle>
<CardDescription>Total pendapatan per bulan</CardDescription>
</CardHeader>
<CardContent>
<div v-if="monthlyRevenueTrend.length > 0" class="space-y-2">
<div v-for="item in monthlyRevenueTrend" :key="item.label" class="flex items-center gap-3">
<span class="text-muted-foreground w-16 text-xs">{{ item.label }}</span>
<div class="bg-muted flex-1 overflow-hidden rounded-full h-4">
<div class="h-full rounded-full bg-green-500 transition-all" :style="{
width: `${Math.max(5, (item.total / Math.max(...monthlyRevenueTrend.map(r => r.total))) * 100)}%`
}" />
</div>
<span class="w-24 text-right text-xs font-medium">{{ formatRupiah(item.total) }}</span>
</div>
</div>
<div v-else class="text-muted-foreground flex h-[150px] items-center justify-center">
Belum ada data
</div>
</CardContent>
</Card>
<!-- Monthly Purchase Trend -->
<Card>
<CardHeader>
<CardTitle class="text-base">Tren Pembelian 6 Bulan</CardTitle>
<CardDescription>Total pembelian per bulan</CardDescription>
</CardHeader>
<CardContent>
<div v-if="monthlyPurchaseTrend.length > 0" class="space-y-2">
<div v-for="item in monthlyPurchaseTrend" :key="item.label" class="flex items-center gap-3">
<span class="text-muted-foreground w-16 text-xs">{{ item.label }}</span>
<div class="bg-muted flex-1 overflow-hidden rounded-full h-4">
<div class="h-full rounded-full bg-blue-500 transition-all" :style="{
width: `${Math.max(5, (item.total / Math.max(...monthlyPurchaseTrend.map(r => r.total))) * 100)}%`
}" />
</div>
<span class="w-24 text-right text-xs font-medium">{{ formatRupiah(item.total) }}</span>
</div>
</div>
<div v-else class="text-muted-foreground flex h-[150px] items-center justify-center">
Belum ada data
</div>
</CardContent>
</Card>
</div>
</div>
</AdminLayout>
</template>