refactor: improve StatCard UI responsiveness and clean up component code formatting
This commit is contained in:
parent
9aeeac00b4
commit
b04dd959f6
@ -1,9 +1,16 @@
|
||||
import { TrendingUp, TrendingDown } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter } from '@/components/ui/card';
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
} from '@/components/ui/card';
|
||||
import { formatCurrency, formatNumber } from '@/lib/formatters';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { StatItem } from '@/types';
|
||||
import { TrendingDown, TrendingUp } from 'lucide-react';
|
||||
|
||||
export interface StatCardProps {
|
||||
title: string;
|
||||
@ -18,35 +25,59 @@ export function StatCard({
|
||||
stat,
|
||||
isCurrency = true,
|
||||
isPercentage = false,
|
||||
className
|
||||
className,
|
||||
}: StatCardProps) {
|
||||
const hasComparison = stat.yesterday !== null;
|
||||
const isPositive = stat.change !== null && stat.change >= 0;
|
||||
|
||||
return (
|
||||
<Card className={cn("@container/card", className)}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardDescription>{title}</CardDescription>
|
||||
</div>
|
||||
<CardTitle className="text-2xl font-bold tabular-nums @[250px]/card:text-3xl">
|
||||
{isPercentage ? `${stat.value}%` : (isCurrency ? formatCurrency(stat.value) : formatNumber(stat.value))}
|
||||
</CardTitle>
|
||||
{hasComparison && (
|
||||
<CardAction>
|
||||
<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" />}
|
||||
<Card className={cn('@container/card relative', className)}>
|
||||
<CardHeader className="gap-3">
|
||||
<div className="flex w-full items-start justify-between gap-3">
|
||||
<CardDescription className="min-w-0 flex-1 break-words leading-tight">
|
||||
{title}
|
||||
</CardDescription>
|
||||
{hasComparison && (
|
||||
<Badge
|
||||
variant="default"
|
||||
className={`shrink-0 flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-bold sm:text-xs ${
|
||||
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" />
|
||||
)}
|
||||
{stat.change === null
|
||||
? '∞'
|
||||
: `${Math.abs(stat.change)}%`}
|
||||
</Badge>
|
||||
</CardAction>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CardTitle className="break-words text-xl sm:text-2xl font-bold tabular-nums tracking-tight">
|
||||
{isPercentage
|
||||
? `${stat.value}%`
|
||||
: isCurrency
|
||||
? formatCurrency(stat.value)
|
||||
: formatNumber(stat.value)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
{hasComparison && (
|
||||
<CardFooter className="flex-col items-start gap-1.5 text-xs">
|
||||
<div className="text-muted-foreground">
|
||||
Kemarin: <span className="font-semibold">{isPercentage ? `${stat.yesterday}%` : (isCurrency ? formatCurrency(stat.yesterday) : formatNumber(stat.yesterday))}</span>
|
||||
<div className="w-full break-words text-muted-foreground leading-relaxed">
|
||||
Kemarin:{' '}
|
||||
<span className="font-semibold break-words inline-block">
|
||||
{isPercentage
|
||||
? `${stat.yesterday}%`
|
||||
: isCurrency
|
||||
? formatCurrency(stat.yesterday)
|
||||
: formatNumber(stat.yesterday)}
|
||||
</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
)}
|
||||
|
||||
@ -1,26 +1,48 @@
|
||||
import { StatCard } from '@/components/cards/stat-card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { formatDate } from '@/lib/formatters';
|
||||
import { analysis } from '@/routes';
|
||||
import type { AnalysisPageProps } from '@/types';
|
||||
import { Head, router, usePage } from '@inertiajs/react';
|
||||
import { CalendarIcon, Filter, FilterX } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { formatDate } from '@/lib/formatters';
|
||||
import { AovTrendChart } from '../components/charts/aov-trend-chart';
|
||||
import { CustomBarChart } from '../components/charts/bar-chart';
|
||||
import { KnobChart } from '../components/charts/knob-chart';
|
||||
import { CustomPieChart } from '../components/charts/pie-chart';
|
||||
import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-chart';
|
||||
import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart';
|
||||
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
|
||||
import { TransactionVolumeChart } from '../components/charts/transaction-volume-chart';
|
||||
import { KnobChart } from '../components/charts/knob-chart';
|
||||
|
||||
export default function Analisa() {
|
||||
const { stats, salesByMonth, revenueVsProfit, transactionVolume, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers, topCategories, filters } =
|
||||
usePage<AnalysisPageProps>().props;
|
||||
const {
|
||||
stats,
|
||||
salesByMonth,
|
||||
revenueVsProfit,
|
||||
transactionVolume,
|
||||
salesByHour,
|
||||
paymentMethods,
|
||||
orderStatuses,
|
||||
orderChannels,
|
||||
topProducts,
|
||||
topCustomers,
|
||||
topCategories,
|
||||
filters,
|
||||
} = usePage<AnalysisPageProps>().props;
|
||||
|
||||
const [period, setPeriod] = useState(filters.period || 'all');
|
||||
const [startDate, setStartDate] = useState(filters.start_date || '');
|
||||
@ -28,19 +50,28 @@ export default function Analisa() {
|
||||
const [isStartOpen, setIsStartOpen] = useState(false);
|
||||
const [isEndOpen, setIsEndOpen] = useState(false);
|
||||
|
||||
|
||||
const formatToYmd = (date: Date) => {
|
||||
return date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, '0') + "-" + String(date.getDate()).padStart(2, '0');
|
||||
return (
|
||||
date.getFullYear() +
|
||||
'-' +
|
||||
String(date.getMonth() + 1).padStart(2, '0') +
|
||||
'-' +
|
||||
String(date.getDate()).padStart(2, '0')
|
||||
);
|
||||
};
|
||||
|
||||
const applyFilters = (newParams: any) => {
|
||||
router.get(analysis().url, {
|
||||
...filters,
|
||||
...newParams
|
||||
}, {
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
});
|
||||
router.get(
|
||||
analysis().url,
|
||||
{
|
||||
...filters,
|
||||
...newParams,
|
||||
},
|
||||
{
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handlePeriodChange = (value: string) => {
|
||||
@ -53,7 +84,11 @@ export default function Analisa() {
|
||||
const handleCustomFilter = () => {
|
||||
if (startDate && endDate) {
|
||||
setPeriod('custom');
|
||||
applyFilters({ period: 'custom', start_date: startDate, end_date: endDate });
|
||||
applyFilters({
|
||||
period: 'custom',
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -69,10 +104,14 @@ export default function Analisa() {
|
||||
return `${baseDesc} (Periode: ${filters.start_date} s/d ${filters.end_date})`;
|
||||
}
|
||||
switch (filters.period) {
|
||||
case 'week': return `${baseDesc}`;
|
||||
case 'month': return `${baseDesc}`;
|
||||
case 'year': return `${baseDesc}`;
|
||||
default: return `${baseDesc}`;
|
||||
case 'week':
|
||||
return `${baseDesc}`;
|
||||
case 'month':
|
||||
return `${baseDesc}`;
|
||||
case 'year':
|
||||
return `${baseDesc}`;
|
||||
default:
|
||||
return `${baseDesc}`;
|
||||
}
|
||||
};
|
||||
|
||||
@ -80,16 +119,22 @@ export default function Analisa() {
|
||||
<>
|
||||
<Head title="Analisa" />
|
||||
|
||||
<div className="flex h-full min-h-screen flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-6 @container/main">
|
||||
|
||||
<div className="@container/main flex h-full min-h-screen flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Analisa</h1>
|
||||
<p className="text-muted-foreground text-sm">Lihat performa bisnis Anda secara mendalam.</p>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Analisa
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Lihat performa bisnis Anda secara mendalam.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Select value={period} onValueChange={handlePeriodChange}>
|
||||
<Select
|
||||
value={period}
|
||||
onValueChange={handlePeriodChange}
|
||||
>
|
||||
<SelectTrigger className="w-[140px] bg-card">
|
||||
<SelectValue placeholder="Pilih Periode" />
|
||||
</SelectTrigger>
|
||||
@ -102,7 +147,10 @@ export default function Analisa() {
|
||||
</Select>
|
||||
|
||||
<div className="flex items-center gap-2 rounded-md border bg-card p-1">
|
||||
<Popover open={isStartOpen} onOpenChange={setIsStartOpen}>
|
||||
<Popover
|
||||
open={isStartOpen}
|
||||
onOpenChange={setIsStartOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@ -111,16 +159,27 @@ export default function Analisa() {
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-3 w-3 text-muted-foreground" />
|
||||
<span className="truncate">
|
||||
{startDate ? formatDate(startDate) : "Mulai"}
|
||||
{startDate
|
||||
? formatDate(startDate)
|
||||
: 'Mulai'}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={startDate ? new Date(startDate) : undefined}
|
||||
selected={
|
||||
startDate
|
||||
? new Date(startDate)
|
||||
: undefined
|
||||
}
|
||||
onSelect={(date) => {
|
||||
setStartDate(date ? formatToYmd(date) : '');
|
||||
setStartDate(
|
||||
date ? formatToYmd(date) : '',
|
||||
);
|
||||
setIsStartOpen(false);
|
||||
}}
|
||||
captionLayout="dropdown"
|
||||
@ -129,9 +188,14 @@ export default function Analisa() {
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<span className="text-muted-foreground text-xs">s/d</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
s/d
|
||||
</span>
|
||||
|
||||
<Popover open={isEndOpen} onOpenChange={setIsEndOpen}>
|
||||
<Popover
|
||||
open={isEndOpen}
|
||||
onOpenChange={setIsEndOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@ -140,16 +204,27 @@ export default function Analisa() {
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-3 w-3 text-muted-foreground" />
|
||||
<span className="truncate">
|
||||
{endDate ? formatDate(endDate) : "Selesai"}
|
||||
{endDate
|
||||
? formatDate(endDate)
|
||||
: 'Selesai'}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={endDate ? new Date(endDate) : undefined}
|
||||
selected={
|
||||
endDate
|
||||
? new Date(endDate)
|
||||
: undefined
|
||||
}
|
||||
onSelect={(date) => {
|
||||
setEndDate(date ? formatToYmd(date) : '');
|
||||
setEndDate(
|
||||
date ? formatToYmd(date) : '',
|
||||
);
|
||||
setIsEndOpen(false);
|
||||
}}
|
||||
captionLayout="dropdown"
|
||||
@ -185,30 +260,55 @@ export default function Analisa() {
|
||||
</div>
|
||||
|
||||
{/* 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-2 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs @sm/main:grid-cols-2 @3xl/main:grid-cols-3 @5xl/main:grid-cols-4 @7xl/main:grid-cols-6 dark:*:data-[slot=card]:bg-card">
|
||||
{[
|
||||
{ title: "Total Penjualan", stat: stats.total_sales, isCurrency: false },
|
||||
{ title: "Total Pendapatan", stat: stats.total_revenue },
|
||||
{ title: "Total Pengeluaran", stat: stats.total_expenses },
|
||||
{ title: "HPP (Modal)", stat: stats.cogs },
|
||||
{ title: "AOV (Avg Order Value)", stat: stats.aov },
|
||||
{ title: "Profit Margin", stat: stats.profit_margin, isPercentage: true },
|
||||
{ title: "Total Diskon", stat: stats.total_discount },
|
||||
{ title: "Laba Kotor", stat: stats.gross_profit },
|
||||
{ title: "Laba Bersih", stat: stats.net_profit },
|
||||
{ title: "Total Gaji", stat: stats.total_payrolls },
|
||||
{ title: "Total Pembelian", stat: stats.total_purchases },
|
||||
{ title: "Produk Terjual", stat: stats.products_sold, isCurrency: false },
|
||||
{ title: "Total Pelanggan", stat: stats.total_customers, isCurrency: false },
|
||||
{
|
||||
title: 'Total Penjualan',
|
||||
stat: stats.total_sales,
|
||||
isCurrency: false,
|
||||
},
|
||||
{
|
||||
title: 'Total Pendapatan',
|
||||
stat: stats.total_revenue,
|
||||
},
|
||||
{
|
||||
title: 'Total Pengeluaran',
|
||||
stat: stats.total_expenses,
|
||||
},
|
||||
{ title: 'HPP (Modal)', stat: stats.cogs },
|
||||
{ title: 'AOV (Avg Order Value)', stat: stats.aov },
|
||||
{
|
||||
title: 'Profit Margin',
|
||||
stat: stats.profit_margin,
|
||||
isPercentage: true,
|
||||
},
|
||||
{ title: 'Total Diskon', stat: stats.total_discount },
|
||||
{ title: 'Laba Kotor', stat: stats.gross_profit },
|
||||
{ title: 'Laba Bersih', stat: stats.net_profit },
|
||||
{ title: 'Total Gaji', stat: stats.total_payrolls },
|
||||
{
|
||||
title: 'Total Pembelian',
|
||||
stat: stats.total_purchases,
|
||||
},
|
||||
{
|
||||
title: 'Produk Terjual',
|
||||
stat: stats.products_sold,
|
||||
isCurrency: false,
|
||||
},
|
||||
{
|
||||
title: 'Total Pelanggan',
|
||||
stat: stats.total_customers,
|
||||
isCurrency: false,
|
||||
},
|
||||
].map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="animate-in fade-in slide-in-from-bottom-4 fill-mode-both"
|
||||
className="animate-in fill-mode-both fade-in slide-in-from-bottom-4"
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
<StatCard
|
||||
{...item}
|
||||
className="transition-all duration-300 hover:scale-[1.03] hover:shadow-xl hover:-translate-y-1"
|
||||
className="transition-all duration-300 hover:-translate-y-1 hover:scale-[1.03] hover:shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -219,12 +319,16 @@ export default function Analisa() {
|
||||
<RevenueVsProfitChart
|
||||
data={revenueVsProfit}
|
||||
title="Pendapatan vs Laba"
|
||||
description={getFilterDescription("Analisis pendapatan kotor dibandingkan dengan laba bersih.")}
|
||||
description={getFilterDescription(
|
||||
'Analisis pendapatan kotor dibandingkan dengan laba bersih.',
|
||||
)}
|
||||
/>
|
||||
<RevenueVsPurchasesChart
|
||||
data={salesByMonth}
|
||||
title="Penjualan vs Belanja"
|
||||
description={getFilterDescription("Perbandingan total nilai penjualan dan belanja stok barang.")}
|
||||
description={getFilterDescription(
|
||||
'Perbandingan total nilai penjualan dan belanja stok barang.',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -233,16 +337,22 @@ export default function Analisa() {
|
||||
<TransactionVolumeChart
|
||||
data={transactionVolume}
|
||||
title="Volume Transaksi"
|
||||
description={getFilterDescription("Total jumlah pesanan yang diproses per bulan.")}
|
||||
description={getFilterDescription(
|
||||
'Total jumlah pesanan yang diproses per bulan.',
|
||||
)}
|
||||
/>
|
||||
<AovTrendChart
|
||||
data={transactionVolume}
|
||||
title="Tren AOV"
|
||||
description={getFilterDescription("Rata-rata nilai belanja per pesanan pelanggan setiap bulannya.")}
|
||||
description={getFilterDescription(
|
||||
'Rata-rata nilai belanja per pesanan pelanggan setiap bulannya.',
|
||||
)}
|
||||
/>
|
||||
<KnobChart
|
||||
title="Repeat Order"
|
||||
description={getFilterDescription("Persentase pelanggan yang kembali berbelanja.")}
|
||||
description={getFilterDescription(
|
||||
'Persentase pelanggan yang kembali berbelanja.',
|
||||
)}
|
||||
percentage={stats.repeat_order_percentage.value}
|
||||
label="Repeat Order"
|
||||
footerText={`${stats.repeat_customers.value} dari ${stats.total_customers.value} pelanggan melakukan repeat order.`}
|
||||
@ -252,11 +362,13 @@ export default function Analisa() {
|
||||
<SalesByHourChart
|
||||
data={salesByHour}
|
||||
title="Jam Sibuk"
|
||||
description={getFilterDescription("Grafik aktivitas transaksi berdasarkan waktu (jam) dalam sehari.")}
|
||||
description={getFilterDescription(
|
||||
'Grafik aktivitas transaksi berdasarkan waktu (jam) dalam sehari.',
|
||||
)}
|
||||
config={{
|
||||
total: {
|
||||
label: "Total Penjualan",
|
||||
color: "var(--chart-1)",
|
||||
label: 'Total Penjualan',
|
||||
color: 'var(--chart-1)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@ -264,25 +376,33 @@ export default function Analisa() {
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 @4xl/main:grid-cols-4">
|
||||
<CustomPieChart
|
||||
title="Metode Pembayaran"
|
||||
description={getFilterDescription("Distribusi transaksi berdasarkan metode pembayaran yang digunakan.")}
|
||||
description={getFilterDescription(
|
||||
'Distribusi transaksi berdasarkan metode pembayaran yang digunakan.',
|
||||
)}
|
||||
data={paymentMethods}
|
||||
colorOffset={0}
|
||||
/>
|
||||
<CustomPieChart
|
||||
title="Status Pesanan"
|
||||
description={getFilterDescription("Status terkini dari seluruh pesanan yang masuk.")}
|
||||
description={getFilterDescription(
|
||||
'Status terkini dari seluruh pesanan yang masuk.',
|
||||
)}
|
||||
data={orderStatuses}
|
||||
colorOffset={120}
|
||||
/>
|
||||
<CustomPieChart
|
||||
title="Channel Pesanan"
|
||||
description={getFilterDescription("Sumber pesanan berdasarkan platform atau saluran penjualan.")}
|
||||
description={getFilterDescription(
|
||||
'Sumber pesanan berdasarkan platform atau saluran penjualan.',
|
||||
)}
|
||||
data={orderChannels}
|
||||
colorOffset={240}
|
||||
/>
|
||||
<CustomPieChart
|
||||
title="Top 5 Kategori"
|
||||
description={getFilterDescription("Kategori produk yang paling banyak menyumbang penjualan.")}
|
||||
description={getFilterDescription(
|
||||
'Kategori produk yang paling banyak menyumbang penjualan.',
|
||||
)}
|
||||
data={topCategories}
|
||||
colorOffset={60}
|
||||
/>
|
||||
@ -291,13 +411,17 @@ export default function Analisa() {
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<CustomBarChart
|
||||
title="Top 5 Produk"
|
||||
description={getFilterDescription("Daftar produk dengan volume penjualan tertinggi.")}
|
||||
description={getFilterDescription(
|
||||
'Daftar produk dengan volume penjualan tertinggi.',
|
||||
)}
|
||||
data={topProducts}
|
||||
colorOffset={45}
|
||||
/>
|
||||
<CustomBarChart
|
||||
title="Top 5 Pelanggan"
|
||||
description={getFilterDescription("Pelanggan dengan total nominal belanja tertinggi (setia).")}
|
||||
description={getFilterDescription(
|
||||
'Pelanggan dengan total nominal belanja tertinggi (setia).',
|
||||
)}
|
||||
data={topCustomers}
|
||||
colorOffset={180}
|
||||
isCurrency={true}
|
||||
|
||||
@ -77,12 +77,12 @@ return {
|
||||
return (
|
||||
<>
|
||||
<Head title="Dasbor" />
|
||||
<div className={`flex h-full min-h-screen flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4 transition-colors duration-1000 @container/main`}>
|
||||
<div className={`flex h-full min-h-screen flex-1 flex-col gap-2 overflow-x-auto rounded-xl p-3 transition-colors duration-1000 @container/main`}>
|
||||
{/* Welcome Section */}
|
||||
<WelcomeCard user={auth.user} time={time} theme={theme} />
|
||||
|
||||
{/* 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-2 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs @sm/main:grid-cols-2 @3xl/main:grid-cols-3 @5xl/main:grid-cols-4 @7xl/main:grid-cols-6 dark:*:data-[slot=card]:bg-card">
|
||||
{[
|
||||
{ title: "Total Penjualan", stat: stats.total_sales, isCurrency: false },
|
||||
{ title: "Total Pendapatan", stat: stats.total_revenue },
|
||||
@ -104,7 +104,7 @@ return {
|
||||
>
|
||||
<StatCard
|
||||
{...item}
|
||||
className="transition-all duration-300 hover:scale-[1.03] hover:shadow-xl hover:-translate-y-1"
|
||||
className="!gap-3 !py-4 [&_[data-slot=card-header]]:!px-4 [&_[data-slot=card-footer]]:!px-4 transition-all duration-300 hover:scale-[1.03] hover:shadow-xl hover:-translate-y-1"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -116,7 +116,7 @@ return {
|
||||
data={salesByHour}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-2 lg:grid-cols-3">
|
||||
<CustomPieChart
|
||||
title="Metode Pembayaran"
|
||||
description="Distribusi transaksi berdasarkan metode pembayaran yang digunakan."
|
||||
@ -137,7 +137,7 @@ return {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-2 lg:grid-cols-2">
|
||||
<CustomBarChart
|
||||
title="Top 5 Produk"
|
||||
description="Daftar produk dengan volume penjualan tertinggi."
|
||||
|
||||
Loading…
Reference in New Issue
Block a user