refactor: modularize dashboard types and streamline UI components for improved stats display
This commit is contained in:
parent
b04ce914ad
commit
5f8718af69
@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Enums\OrderChannel;
|
|
||||||
use App\Enums\OrderStatus;
|
|
||||||
use App\Enums\PaymentMethod;
|
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\OrderItem;
|
use App\Models\OrderItem;
|
||||||
@ -20,114 +17,49 @@ public function __invoke()
|
|||||||
$yesterday = Carbon::yesterday();
|
$yesterday = Carbon::yesterday();
|
||||||
|
|
||||||
$stats = [
|
$stats = [
|
||||||
'total_penjualan' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->count()),
|
'total_sales' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->count()),
|
||||||
'total_pendapatan' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('total')),
|
'total_revenue' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('total')),
|
||||||
'total_pengeluaran' => $this->getStats($today, $yesterday, fn ($date) => Expense::whereDate('created_at', $date)->sum('amount')),
|
'total_expenses' => $this->getStats($today, $yesterday, fn ($date) => Expense::whereDate('created_at', $date)->sum('amount')),
|
||||||
'hpp' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('hpp')),
|
'cogs' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('hpp')),
|
||||||
'total_diskon' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('discount')),
|
'total_discount' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->sum('discount')),
|
||||||
'total_pembelian' => $this->getStats($today, $yesterday, fn ($date) => Purchase::whereDate('created_at', $date)->sum('total')),
|
'total_purchases' => $this->getStats($today, $yesterday, fn ($date) => Purchase::whereDate('created_at', $date)->sum('total')),
|
||||||
'produk_terjual' => $this->getStats($today, $yesterday, fn ($date) => OrderItem::whereHas('order', fn ($q) => $q->whereDate('created_at', $date))->sum('qty')),
|
'products_sold' => $this->getStats($today, $yesterday, fn ($date) => OrderItem::whereHas('order', fn ($q) => $q->whereDate('created_at', $date))->sum('qty')),
|
||||||
'total_pelanggan' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->distinct('customer_name')->count('customer_name')),
|
'total_customers' => $this->getStats($today, $yesterday, fn ($date) => Order::whereDate('created_at', $date)->distinct('customer_name')->count('customer_name')),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Complex stats
|
$stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']);
|
||||||
$stats['aov'] = $this->calculateAov($stats['total_pendapatan'], $stats['total_penjualan']);
|
$stats['gross_profit'] = $this->calculateDiff($stats['total_revenue'], $stats['cogs']);
|
||||||
$stats['laba_kotor'] = $this->calculateDiff($stats['total_pendapatan'], $stats['hpp']);
|
$stats['net_profit'] = $this->calculateDiff($stats['gross_profit'], $stats['total_expenses']);
|
||||||
$stats['laba_bersih'] = $this->calculateDiff($stats['laba_kotor'], $stats['total_pengeluaran']);
|
$stats['profit_margin'] = $this->calculateMargin($stats['net_profit'], $stats['total_revenue']);
|
||||||
$stats['profit_margin'] = $this->calculateMargin($stats['laba_bersih'], $stats['total_pendapatan']);
|
|
||||||
|
|
||||||
// Chart Data
|
|
||||||
$stats['charts'] = [
|
|
||||||
'jam_sibuk' => $this->getJamSibuk($today, $yesterday),
|
|
||||||
'order_by_payment' => $this->getOrderByEnum($today, 'payment_method', PaymentMethod::class),
|
|
||||||
'order_by_status' => $this->getOrderByEnum($today, 'order_status', OrderStatus::class),
|
|
||||||
'order_by_channel' => $this->getOrderByEnum($today, 'order_channel', OrderChannel::class),
|
|
||||||
'top_products' => $this->getTopProducts($today),
|
|
||||||
];
|
|
||||||
|
|
||||||
return Inertia::render('dashboard', [
|
return Inertia::render('dashboard', [
|
||||||
'stats' => $stats,
|
'stats' => $stats,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getJamSibuk($today, $yesterday)
|
private function getStats($today, $yesterday, $callback): array
|
||||||
{
|
|
||||||
$todayData = Order::whereDate('created_at', $today)
|
|
||||||
->selectRaw('HOUR(created_at) as hour, count(*) as count')
|
|
||||||
->groupBy('hour')
|
|
||||||
->pluck('count', 'hour')
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
$yesterdayData = Order::whereDate('created_at', $yesterday)
|
|
||||||
->selectRaw('HOUR(created_at) as hour, count(*) as count')
|
|
||||||
->groupBy('hour')
|
|
||||||
->pluck('count', 'hour')
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
$chartData = [];
|
|
||||||
for ($i = 0; $i < 24; $i++) {
|
|
||||||
$chartData[] = [
|
|
||||||
'hour' => sprintf('%02d:00', $i),
|
|
||||||
'today' => $todayData[$i] ?? 0,
|
|
||||||
'yesterday' => $yesterdayData[$i] ?? 0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $chartData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getOrderByEnum($date, $column, $enumClass)
|
|
||||||
{
|
|
||||||
$data = Order::whereDate('created_at', $date)
|
|
||||||
->selectRaw("$column, count(*) as count")
|
|
||||||
->groupBy($column)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return $data->map(function ($item) use ($column, $enumClass) {
|
|
||||||
$enumValue = $item->$column;
|
|
||||||
$label = $enumValue instanceof $enumClass ? $enumValue->label() : $enumValue;
|
|
||||||
|
|
||||||
return [
|
|
||||||
'name' => $label,
|
|
||||||
'value' => $item->count,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getTopProducts($date)
|
|
||||||
{
|
|
||||||
return OrderItem::whereHas('order', fn ($q) => $q->whereDate('created_at', $date))
|
|
||||||
->with('product:id,name')
|
|
||||||
->selectRaw('product_id, sum(qty) as total_qty')
|
|
||||||
->groupBy('product_id')
|
|
||||||
->orderByDesc('total_qty')
|
|
||||||
->limit(5)
|
|
||||||
->get()
|
|
||||||
->map(fn ($item) => [
|
|
||||||
'name' => $item->product->name ?? 'Unknown',
|
|
||||||
'qty' => (int) $item->total_qty,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getStats($today, $yesterday, $callback)
|
|
||||||
{
|
{
|
||||||
$todayVal = $callback($today);
|
$todayVal = $callback($today);
|
||||||
$yesterdayVal = $callback($yesterday);
|
$yesterdayVal = $callback($yesterday);
|
||||||
|
|
||||||
|
$diff = $todayVal - $yesterdayVal;
|
||||||
|
$change = $this->calculatePercentageChange($todayVal, $yesterdayVal);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'value' => $todayVal,
|
'value' => $todayVal,
|
||||||
'yesterday' => $yesterdayVal,
|
'yesterday' => $yesterdayVal,
|
||||||
'change' => $this->calculatePercentageChange($todayVal, $yesterdayVal),
|
'diff' => $diff,
|
||||||
|
'change' => $change,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function calculatePercentageChange($current, $previous)
|
private function calculatePercentageChange($current, $previous): ?string
|
||||||
{
|
{
|
||||||
if ($previous == 0) {
|
if ($previous == 0) {
|
||||||
return $current > 0 ? 100 : 0;
|
return $current == 0 ? 0 : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return round((($current - $previous) / $previous) * 100, 2);
|
return round((($current - $previous) / abs($previous)) * 100, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function calculateAov($revenue, $sales)
|
private function calculateAov($revenue, $sales)
|
||||||
@ -142,7 +74,7 @@ private function calculateAov($revenue, $sales)
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function calculateDiff($a, $b)
|
private function calculateDiff($a, $b): array
|
||||||
{
|
{
|
||||||
$todayVal = $a['value'] - $b['value'];
|
$todayVal = $a['value'] - $b['value'];
|
||||||
$yesterdayVal = $a['yesterday'] - $b['yesterday'];
|
$yesterdayVal = $a['yesterday'] - $b['yesterday'];
|
||||||
@ -154,7 +86,7 @@ private function calculateDiff($a, $b)
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function calculateMargin($profit, $revenue)
|
private function calculateMargin($profit, $revenue): array
|
||||||
{
|
{
|
||||||
$todayVal = $revenue['value'] > 0 ? ($profit['value'] / $revenue['value']) * 100 : 0;
|
$todayVal = $revenue['value'] > 0 ? ($profit['value'] / $revenue['value']) * 100 : 0;
|
||||||
$yesterdayVal = $revenue['yesterday'] > 0 ? ($profit['yesterday'] / $revenue['yesterday']) * 100 : 0;
|
$yesterdayVal = $revenue['yesterday'] > 0 ? ($profit['yesterday'] / $revenue['yesterday']) * 100 : 0;
|
||||||
|
|||||||
@ -1,60 +1,16 @@
|
|||||||
import { Head, usePage } from '@inertiajs/react';
|
import { Head, usePage } from '@inertiajs/react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { PlaceholderPattern } from '@/components/ui/placeholder-pattern';
|
|
||||||
import { dashboard } from '@/routes';
|
import { dashboard } from '@/routes';
|
||||||
import { Auth } from '@/types';
|
import { StatItem, DashboardPageProps } from '@/types';
|
||||||
import {
|
import {
|
||||||
Sun, Moon, Cloud, Trees, CloudRain, Stars, Bird,
|
Sun, Moon, Cloud, Trees, Stars, Bird,
|
||||||
TrendingUp, TrendingDown, ShoppingBag, DollarSign,
|
TrendingUp, TrendingDown, ShoppingBag, DollarSign,
|
||||||
Wallet, Receipt, Percent, BarChart3, CreditCard,
|
Wallet, Receipt, Percent, BarChart3, CreditCard,
|
||||||
ArrowUpRight, ArrowDownRight, Package, Users, ShoppingCart
|
ArrowUpRight, ArrowDownRight, Package, Users, ShoppingCart
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter, CardContent } from '@/components/ui/card';
|
import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
|
||||||
BarChart, Bar, XAxis, YAxis, CartesianGrid,
|
|
||||||
ResponsiveContainer, AreaChart, Area,
|
|
||||||
PieChart, Pie, Cell, LabelList, LineChart, Line, Sector, Label
|
|
||||||
} from 'recharts';
|
|
||||||
import {
|
|
||||||
ChartContainer, ChartTooltip, ChartTooltipContent,
|
|
||||||
ChartLegend, ChartLegendContent, type ChartConfig
|
|
||||||
} from '@/components/ui/chart';
|
|
||||||
|
|
||||||
interface StatItem {
|
|
||||||
value: number;
|
|
||||||
yesterday: number;
|
|
||||||
change: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DashboardStats {
|
|
||||||
total_penjualan: StatItem;
|
|
||||||
total_pendapatan: StatItem;
|
|
||||||
total_pengeluaran: StatItem;
|
|
||||||
hpp: StatItem;
|
|
||||||
aov: StatItem;
|
|
||||||
profit_margin: StatItem;
|
|
||||||
total_diskon: StatItem;
|
|
||||||
laba_kotor: StatItem;
|
|
||||||
laba_bersih: StatItem;
|
|
||||||
total_pembelian: StatItem;
|
|
||||||
produk_terjual: StatItem;
|
|
||||||
total_pelanggan: StatItem;
|
|
||||||
charts: {
|
|
||||||
jam_sibuk: Array<{ hour: string; today: number; yesterday: number }>;
|
|
||||||
order_by_payment: Array<{ name: string; value: number }>;
|
|
||||||
order_by_status: Array<{ name: string; value: number }>;
|
|
||||||
order_by_channel: Array<{ name: string; value: number }>;
|
|
||||||
top_products: Array<{ name: string; qty: number }>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageProps {
|
|
||||||
auth: Auth;
|
|
||||||
stats: DashboardStats;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DynamicScene = ({ hour }: { hour: number }) => {
|
const DynamicScene = ({ hour }: { hour: number }) => {
|
||||||
if (hour >= 5 && hour < 11) {
|
if (hour >= 5 && hour < 11) {
|
||||||
@ -95,7 +51,7 @@ const DynamicScene = ({ hour }: { hour: number }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { auth, stats } = usePage<PageProps>().props;
|
const { auth, stats } = usePage<DashboardPageProps>().props;
|
||||||
const [time, setTime] = useState(new Date());
|
const [time, setTime] = useState(new Date());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -205,25 +161,15 @@ export default function Dashboard() {
|
|||||||
{isPercentage ? `${stat.value}%` : (isCurrency ? formatCurrency(stat.value) : formatNumber(stat.value))}
|
{isPercentage ? `${stat.value}%` : (isCurrency ? formatCurrency(stat.value) : formatNumber(stat.value))}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardAction>
|
<CardAction>
|
||||||
<Badge variant={isPositive ? "default" : "destructive"} className="flex gap-1 px-1.5 py-0.5 text-xs font-bold">
|
<Badge variant="default" className={`flex gap-1 px-1.5 py-0.5 text-xs font-bold ${isPositive ? 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300' : 'bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300'}`}>
|
||||||
{isPositive ? <TrendingUp className="size-3" /> : <TrendingDown className="size-3" />}
|
{isPositive ? <TrendingUp className="size-3" /> : <TrendingDown className="size-3" />}
|
||||||
{Math.abs(stat.change)}%
|
{stat.change === null
|
||||||
|
? '∞'
|
||||||
|
: `${Math.abs(stat.change)}%`}
|
||||||
</Badge>
|
</Badge>
|
||||||
</CardAction>
|
</CardAction>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="flex-col items-start gap-1.5 text-xs">
|
<CardFooter className="flex-col items-start gap-1.5 text-xs">
|
||||||
<div className="flex items-center gap-1.5 font-medium">
|
|
||||||
{isPositive ? (
|
|
||||||
<span className="flex items-center gap-1 text-emerald-600 dark:text-emerald-400">
|
|
||||||
<ArrowUpRight className="size-3" /> Meningkat
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="flex items-center gap-1 text-rose-600 dark:text-rose-400">
|
|
||||||
<ArrowDownRight className="size-3" /> Menurun
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-muted-foreground">vs kemarin</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
Kemarin: <span className="font-semibold">{isPercentage ? `${stat.yesterday}%` : (isCurrency ? formatCurrency(stat.yesterday) : formatNumber(stat.yesterday))}</span>
|
Kemarin: <span className="font-semibold">{isPercentage ? `${stat.yesterday}%` : (isCurrency ? formatCurrency(stat.yesterday) : formatNumber(stat.yesterday))}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -267,18 +213,18 @@ export default function Dashboard() {
|
|||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
<div className="grid grid-cols-1 gap-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @4xl/main:grid-cols-4 dark:*:data-[slot=card]:bg-card">
|
<div className="grid grid-cols-1 gap-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @4xl/main:grid-cols-4 dark:*:data-[slot=card]:bg-card">
|
||||||
{[
|
{[
|
||||||
{ title: "Total Penjualan", stat: stats.total_penjualan, icon: ShoppingBag, isCurrency: false },
|
{ title: "Total Penjualan", stat: stats.total_sales, icon: ShoppingBag, isCurrency: false },
|
||||||
{ title: "Total Pendapatan", stat: stats.total_pendapatan, icon: DollarSign },
|
{ title: "Total Pendapatan", stat: stats.total_revenue, icon: DollarSign },
|
||||||
{ title: "Total Pengeluaran", stat: stats.total_pengeluaran, icon: Wallet },
|
{ title: "Total Pengeluaran", stat: stats.total_expenses, icon: Wallet },
|
||||||
{ title: "HPP (Modal)", stat: stats.hpp, icon: Receipt },
|
{ title: "HPP (Modal)", stat: stats.cogs, icon: Receipt },
|
||||||
{ title: "AOV (Avg Order Value)", stat: stats.aov, icon: CreditCard },
|
{ title: "AOV (Avg Order Value)", stat: stats.aov, icon: CreditCard },
|
||||||
{ title: "Profit Margin", stat: stats.profit_margin, icon: Percent, isPercentage: true },
|
{ title: "Profit Margin", stat: stats.profit_margin, icon: Percent, isPercentage: true },
|
||||||
{ title: "Total Diskon", stat: stats.total_diskon, icon: Percent },
|
{ title: "Total Diskon", stat: stats.total_discount, icon: Percent },
|
||||||
{ title: "Laba Kotor", stat: stats.laba_kotor, icon: BarChart3 },
|
{ title: "Laba Kotor", stat: stats.gross_profit, icon: BarChart3 },
|
||||||
{ title: "Laba Bersih", stat: stats.laba_bersih, icon: TrendingUp },
|
{ title: "Laba Bersih", stat: stats.net_profit, icon: TrendingUp },
|
||||||
{ title: "Total Pembelian", stat: stats.total_pembelian, icon: ShoppingCart },
|
{ title: "Total Pembelian", stat: stats.total_purchases, icon: ShoppingCart },
|
||||||
{ title: "Produk Terjual", stat: stats.produk_terjual, icon: Package, isCurrency: false },
|
{ title: "Produk Terjual", stat: stats.products_sold, icon: Package, isCurrency: false },
|
||||||
{ title: "Total Pelanggan", stat: stats.total_pelanggan, icon: Users, isCurrency: false },
|
{ title: "Total Pelanggan", stat: stats.total_customers, icon: Users, isCurrency: false },
|
||||||
].map((item, index) => (
|
].map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
@ -292,178 +238,6 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Charts Section */}
|
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
||||||
{/* Jam Sibuk - Multiple Line Chart Pattern */}
|
|
||||||
<Card className="col-span-full transition-all duration-500 hover:shadow-xl animate-in fade-in slide-in-from-bottom-8 fill-mode-both" style={{ animationDelay: '1200ms' }}>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Jam Sibuk (Pesanan per Jam)</CardTitle>
|
|
||||||
<CardDescription>Perbandingan aktivitas hari ini vs kemarin</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer
|
|
||||||
config={{
|
|
||||||
today: { label: "Hari Ini", color: "var(--chart-1)" },
|
|
||||||
yesterday: { label: "Kemarin", color: "var(--chart-2)" },
|
|
||||||
}}
|
|
||||||
className="h-[300px] w-full"
|
|
||||||
>
|
|
||||||
<LineChart
|
|
||||||
accessibilityLayer
|
|
||||||
data={stats.charts.jam_sibuk}
|
|
||||||
margin={{ left: 12, right: 12 }}
|
|
||||||
>
|
|
||||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
||||||
<XAxis
|
|
||||||
dataKey="hour"
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={false}
|
|
||||||
tickMargin={8}
|
|
||||||
/>
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
|
||||||
<Line
|
|
||||||
dataKey="today"
|
|
||||||
type="monotone"
|
|
||||||
stroke="var(--color-today)"
|
|
||||||
strokeWidth={3}
|
|
||||||
dot={{ r: 4, fill: "var(--color-today)" }}
|
|
||||||
activeDot={{ r: 6 }}
|
|
||||||
/>
|
|
||||||
<Line
|
|
||||||
dataKey="yesterday"
|
|
||||||
type="monotone"
|
|
||||||
stroke="var(--color-yesterday)"
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeDasharray="5 5"
|
|
||||||
dot={false}
|
|
||||||
/>
|
|
||||||
<ChartLegend content={<ChartLegendContent />} />
|
|
||||||
</LineChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter>
|
|
||||||
<div className="flex w-full items-start gap-2 text-sm text-muted-foreground">
|
|
||||||
<TrendingUp className="h-4 w-4 text-emerald-500" />
|
|
||||||
<span>Menampilkan data pesanan per jam untuk mengidentifikasi waktu operasional tersibuk.</span>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Order Distribution - Pie Chart Label List Pattern */}
|
|
||||||
<Card className="flex flex-col transition-all duration-500 hover:shadow-xl animate-in fade-in slide-in-from-bottom-8 fill-mode-both" style={{ animationDelay: '1300ms' }}>
|
|
||||||
<CardHeader className="items-center pb-0">
|
|
||||||
<CardTitle>Metode Pembayaran</CardTitle>
|
|
||||||
<CardDescription>Distribusi transaksi hari ini</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex-1 pb-0">
|
|
||||||
<ChartContainer
|
|
||||||
config={Object.fromEntries(stats.charts.order_by_payment.map((item, i) => [
|
|
||||||
item.name.toLowerCase().replace(/[^a-z0-h]/g, ''),
|
|
||||||
{ label: item.name, color: `var(--chart-${(i % 5) + 1})` }
|
|
||||||
]))}
|
|
||||||
className="mx-auto aspect-square max-h-[300px] [&_.recharts-text]:fill-foreground"
|
|
||||||
>
|
|
||||||
<PieChart>
|
|
||||||
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
|
||||||
<Pie
|
|
||||||
data={stats.charts.order_by_payment.map((item, i) => ({
|
|
||||||
...item,
|
|
||||||
fill: `var(--color-${item.name.toLowerCase().replace(/[^a-z0-h]/g, '')})`
|
|
||||||
}))}
|
|
||||||
dataKey="value"
|
|
||||||
nameKey="name"
|
|
||||||
>
|
|
||||||
<LabelList
|
|
||||||
dataKey="name"
|
|
||||||
className="fill-foreground font-bold"
|
|
||||||
stroke="none"
|
|
||||||
fontSize={12}
|
|
||||||
/>
|
|
||||||
</Pie>
|
|
||||||
</PieChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex-col gap-2 text-sm">
|
|
||||||
<div className="flex items-center gap-2 font-medium">
|
|
||||||
<TrendingUp className="h-4 w-4" /> Distribusi pembayaran real-time
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Order Channel - Donut Active Pattern */}
|
|
||||||
<Card className="flex flex-col transition-all duration-500 hover:shadow-xl animate-in fade-in slide-in-from-bottom-8 fill-mode-both" style={{ animationDelay: '1400ms' }}>
|
|
||||||
<CardHeader className="items-center pb-0">
|
|
||||||
<CardTitle>Saluran Pesanan</CardTitle>
|
|
||||||
<CardDescription>Sumber pesanan masuk</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex-1 pb-0">
|
|
||||||
<ChartContainer
|
|
||||||
config={Object.fromEntries(stats.charts.order_by_channel.map((item, i) => [
|
|
||||||
item.name.toLowerCase().replace(/[^a-z0-h]/g, ''),
|
|
||||||
{ label: item.name, color: `var(--chart-${((i + 2) % 5) + 1})` }
|
|
||||||
]))}
|
|
||||||
className="mx-auto aspect-square max-h-[300px]"
|
|
||||||
>
|
|
||||||
<PieChart>
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
||||||
<Pie
|
|
||||||
data={stats.charts.order_by_channel.map((item, i) => ({
|
|
||||||
...item,
|
|
||||||
fill: `var(--color-${item.name.toLowerCase().replace(/[^a-z0-h]/g, '')})`
|
|
||||||
}))}
|
|
||||||
dataKey="value"
|
|
||||||
nameKey="name"
|
|
||||||
innerRadius={60}
|
|
||||||
strokeWidth={5}
|
|
||||||
activeShape={({ outerRadius = 0, ...props }: any) => (
|
|
||||||
<Sector {...props} outerRadius={outerRadius + 10} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</PieChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex-col gap-2 text-sm">
|
|
||||||
<div className="flex items-center gap-2 font-medium">
|
|
||||||
<TrendingUp className="h-4 w-4" /> Performa saluran penjualan
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Top Products - Multiple Bar Pattern (Adapted) */}
|
|
||||||
<Card className="col-span-full transition-all duration-500 hover:shadow-xl animate-in fade-in slide-in-from-bottom-8 fill-mode-both" style={{ animationDelay: '1500ms' }}>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Top 5 Produk Terlaris</CardTitle>
|
|
||||||
<CardDescription>Berdasarkan kuantitas yang terjual hari ini</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer
|
|
||||||
config={{
|
|
||||||
qty: { label: "Terjual", color: "var(--chart-1)" },
|
|
||||||
}}
|
|
||||||
className="h-[350px] w-full"
|
|
||||||
>
|
|
||||||
<BarChart accessibilityLayer data={stats.charts.top_products}>
|
|
||||||
<CartesianGrid vertical={false} strokeDasharray="3 3" />
|
|
||||||
<XAxis
|
|
||||||
dataKey="name"
|
|
||||||
tickLine={false}
|
|
||||||
tickMargin={10}
|
|
||||||
axisLine={false}
|
|
||||||
tickFormatter={(value) => value.length > 15 ? value.slice(0, 15) + '...' : value}
|
|
||||||
/>
|
|
||||||
<ChartTooltip cursor={false} content={<ChartTooltipContent indicator="dashed" />} />
|
|
||||||
<Bar
|
|
||||||
dataKey="qty"
|
|
||||||
fill="var(--color-qty)"
|
|
||||||
radius={8}
|
|
||||||
label={{ position: 'top', fill: 'var(--foreground)', fontSize: 12 }}
|
|
||||||
/>
|
|
||||||
</BarChart>
|
|
||||||
</ChartContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
28
resources/js/types/dashboard.ts
Normal file
28
resources/js/types/dashboard.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Auth } from './auth';
|
||||||
|
|
||||||
|
export interface StatItem {
|
||||||
|
value: number;
|
||||||
|
yesterday: number;
|
||||||
|
change: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardStats {
|
||||||
|
total_sales: StatItem;
|
||||||
|
total_revenue: StatItem;
|
||||||
|
total_expenses: StatItem;
|
||||||
|
cogs: StatItem;
|
||||||
|
aov: StatItem;
|
||||||
|
profit_margin: StatItem;
|
||||||
|
total_discount: StatItem;
|
||||||
|
gross_profit: StatItem;
|
||||||
|
net_profit: StatItem;
|
||||||
|
total_purchases: StatItem;
|
||||||
|
products_sold: StatItem;
|
||||||
|
total_customers: StatItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardPageProps {
|
||||||
|
auth: Auth;
|
||||||
|
stats: DashboardStats;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
@ -9,4 +9,4 @@ export type * from './purchase';
|
|||||||
export type * from './general-setting';
|
export type * from './general-setting';
|
||||||
export type * from './activity';
|
export type * from './activity';
|
||||||
export type * from './system-log';
|
export type * from './system-log';
|
||||||
|
export type * from './dashboard';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user