refactor: improve StatCard UI responsiveness and clean up component code formatting

This commit is contained in:
Yoga Pangestu 2026-04-30 10:27:55 +07:00
parent 9aeeac00b4
commit b04dd959f6
3 changed files with 245 additions and 90 deletions

View File

@ -1,9 +1,16 @@
import { TrendingUp, TrendingDown } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; 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 { formatCurrency, formatNumber } from '@/lib/formatters';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import type { StatItem } from '@/types'; import type { StatItem } from '@/types';
import { TrendingDown, TrendingUp } from 'lucide-react';
export interface StatCardProps { export interface StatCardProps {
title: string; title: string;
@ -18,35 +25,59 @@ export function StatCard({
stat, stat,
isCurrency = true, isCurrency = true,
isPercentage = false, isPercentage = false,
className className,
}: StatCardProps) { }: StatCardProps) {
const hasComparison = stat.yesterday !== null; const hasComparison = stat.yesterday !== null;
const isPositive = stat.change !== null && stat.change >= 0; const isPositive = stat.change !== null && stat.change >= 0;
return ( return (
<Card className={cn("@container/card", className)}> <Card className={cn('@container/card relative', className)}>
<CardHeader> <CardHeader className="gap-3">
<div className="flex items-center justify-between"> <div className="flex w-full items-start justify-between gap-3">
<CardDescription>{title}</CardDescription> <CardDescription className="min-w-0 flex-1 break-words leading-tight">
</div> {title}
<CardTitle className="text-2xl font-bold tabular-nums @[250px]/card:text-3xl"> </CardDescription>
{isPercentage ? `${stat.value}%` : (isCurrency ? formatCurrency(stat.value) : formatNumber(stat.value))} {hasComparison && (
</CardTitle> <Badge
{hasComparison && ( variant="default"
<CardAction> className={`shrink-0 flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-bold sm:text-xs ${
<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
{isPositive ? <TrendingUp className="size-3" /> : <TrendingDown className="size-3" />} ? '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 {stat.change === null
? '∞' ? '∞'
: `${Math.abs(stat.change)}%`} : `${Math.abs(stat.change)}%`}
</Badge> </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> </CardHeader>
{hasComparison && ( {hasComparison && (
<CardFooter className="flex-col items-start gap-1.5 text-xs"> <CardFooter className="flex-col items-start gap-1.5 text-xs">
<div className="text-muted-foreground"> <div className="w-full break-words text-muted-foreground leading-relaxed">
Kemarin: <span className="font-semibold">{isPercentage ? `${stat.yesterday}%` : (isCurrency ? formatCurrency(stat.yesterday) : formatNumber(stat.yesterday))}</span> Kemarin:{' '}
<span className="font-semibold break-words inline-block">
{isPercentage
? `${stat.yesterday}%`
: isCurrency
? formatCurrency(stat.yesterday)
: formatNumber(stat.yesterday)}
</span>
</div> </div>
</CardFooter> </CardFooter>
)} )}

View File

@ -1,26 +1,48 @@
import { StatCard } from '@/components/cards/stat-card'; import { StatCard } from '@/components/cards/stat-card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar'; import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import {
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; 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 { analysis } from '@/routes';
import type { AnalysisPageProps } from '@/types'; import type { AnalysisPageProps } from '@/types';
import { Head, router, usePage } from '@inertiajs/react'; import { Head, router, usePage } from '@inertiajs/react';
import { CalendarIcon, Filter, FilterX } from 'lucide-react'; import { CalendarIcon, Filter, FilterX } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { formatDate } from '@/lib/formatters';
import { AovTrendChart } from '../components/charts/aov-trend-chart'; import { AovTrendChart } from '../components/charts/aov-trend-chart';
import { CustomBarChart } from '../components/charts/bar-chart'; import { CustomBarChart } from '../components/charts/bar-chart';
import { KnobChart } from '../components/charts/knob-chart';
import { CustomPieChart } from '../components/charts/pie-chart'; import { CustomPieChart } from '../components/charts/pie-chart';
import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-chart'; import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-chart';
import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart'; import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart';
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart'; import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
import { TransactionVolumeChart } from '../components/charts/transaction-volume-chart'; import { TransactionVolumeChart } from '../components/charts/transaction-volume-chart';
import { KnobChart } from '../components/charts/knob-chart';
export default function Analisa() { export default function Analisa() {
const { stats, salesByMonth, revenueVsProfit, transactionVolume, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers, topCategories, filters } = const {
usePage<AnalysisPageProps>().props; stats,
salesByMonth,
revenueVsProfit,
transactionVolume,
salesByHour,
paymentMethods,
orderStatuses,
orderChannels,
topProducts,
topCustomers,
topCategories,
filters,
} = usePage<AnalysisPageProps>().props;
const [period, setPeriod] = useState(filters.period || 'all'); const [period, setPeriod] = useState(filters.period || 'all');
const [startDate, setStartDate] = useState(filters.start_date || ''); const [startDate, setStartDate] = useState(filters.start_date || '');
@ -28,19 +50,28 @@ export default function Analisa() {
const [isStartOpen, setIsStartOpen] = useState(false); const [isStartOpen, setIsStartOpen] = useState(false);
const [isEndOpen, setIsEndOpen] = useState(false); const [isEndOpen, setIsEndOpen] = useState(false);
const formatToYmd = (date: Date) => { 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) => { const applyFilters = (newParams: any) => {
router.get(analysis().url, { router.get(
...filters, analysis().url,
...newParams {
}, { ...filters,
preserveState: true, ...newParams,
preserveScroll: true, },
}); {
preserveState: true,
preserveScroll: true,
},
);
}; };
const handlePeriodChange = (value: string) => { const handlePeriodChange = (value: string) => {
@ -53,7 +84,11 @@ export default function Analisa() {
const handleCustomFilter = () => { const handleCustomFilter = () => {
if (startDate && endDate) { if (startDate && endDate) {
setPeriod('custom'); 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})`; return `${baseDesc} (Periode: ${filters.start_date} s/d ${filters.end_date})`;
} }
switch (filters.period) { switch (filters.period) {
case 'week': return `${baseDesc}`; case 'week':
case 'month': return `${baseDesc}`; return `${baseDesc}`;
case 'year': return `${baseDesc}`; case 'month':
default: return `${baseDesc}`; return `${baseDesc}`;
case 'year':
return `${baseDesc}`;
default:
return `${baseDesc}`;
} }
}; };
@ -80,16 +119,22 @@ export default function Analisa() {
<> <>
<Head title="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 className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Analisa</h1> <h1 className="text-3xl font-bold tracking-tight">
<p className="text-muted-foreground text-sm">Lihat performa bisnis Anda secara mendalam.</p> Analisa
</h1>
<p className="text-sm text-muted-foreground">
Lihat performa bisnis Anda secara mendalam.
</p>
</div> </div>
<div className="flex flex-wrap items-center gap-2"> <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"> <SelectTrigger className="w-[140px] bg-card">
<SelectValue placeholder="Pilih Periode" /> <SelectValue placeholder="Pilih Periode" />
</SelectTrigger> </SelectTrigger>
@ -102,7 +147,10 @@ export default function Analisa() {
</Select> </Select>
<div className="flex items-center gap-2 rounded-md border bg-card p-1"> <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> <PopoverTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
@ -111,16 +159,27 @@ export default function Analisa() {
> >
<CalendarIcon className="mr-2 h-3 w-3 text-muted-foreground" /> <CalendarIcon className="mr-2 h-3 w-3 text-muted-foreground" />
<span className="truncate"> <span className="truncate">
{startDate ? formatDate(startDate) : "Mulai"} {startDate
? formatDate(startDate)
: 'Mulai'}
</span> </span>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start"> <PopoverContent
className="w-auto p-0"
align="start"
>
<Calendar <Calendar
mode="single" mode="single"
selected={startDate ? new Date(startDate) : undefined} selected={
startDate
? new Date(startDate)
: undefined
}
onSelect={(date) => { onSelect={(date) => {
setStartDate(date ? formatToYmd(date) : ''); setStartDate(
date ? formatToYmd(date) : '',
);
setIsStartOpen(false); setIsStartOpen(false);
}} }}
captionLayout="dropdown" captionLayout="dropdown"
@ -129,9 +188,14 @@ export default function Analisa() {
</PopoverContent> </PopoverContent>
</Popover> </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> <PopoverTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
@ -140,16 +204,27 @@ export default function Analisa() {
> >
<CalendarIcon className="mr-2 h-3 w-3 text-muted-foreground" /> <CalendarIcon className="mr-2 h-3 w-3 text-muted-foreground" />
<span className="truncate"> <span className="truncate">
{endDate ? formatDate(endDate) : "Selesai"} {endDate
? formatDate(endDate)
: 'Selesai'}
</span> </span>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start"> <PopoverContent
className="w-auto p-0"
align="start"
>
<Calendar <Calendar
mode="single" mode="single"
selected={endDate ? new Date(endDate) : undefined} selected={
endDate
? new Date(endDate)
: undefined
}
onSelect={(date) => { onSelect={(date) => {
setEndDate(date ? formatToYmd(date) : ''); setEndDate(
date ? formatToYmd(date) : '',
);
setIsEndOpen(false); setIsEndOpen(false);
}} }}
captionLayout="dropdown" captionLayout="dropdown"
@ -185,30 +260,55 @@ export default function Analisa() {
</div> </div>
{/* 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-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 Penjualan',
{ title: "Total Pengeluaran", stat: stats.total_expenses }, stat: stats.total_sales,
{ title: "HPP (Modal)", stat: stats.cogs }, isCurrency: false,
{ 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: 'Total Pendapatan',
{ title: "Laba Kotor", stat: stats.gross_profit }, stat: stats.total_revenue,
{ title: "Laba Bersih", stat: stats.net_profit }, },
{ title: "Total Gaji", stat: stats.total_payrolls }, {
{ title: "Total Pembelian", stat: stats.total_purchases }, title: 'Total Pengeluaran',
{ title: "Produk Terjual", stat: stats.products_sold, isCurrency: false }, stat: stats.total_expenses,
{ title: "Total Pelanggan", stat: stats.total_customers, isCurrency: false }, },
{ 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) => ( ].map((item, index) => (
<div <div
key={index} 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` }} style={{ animationDelay: `${index * 100}ms` }}
> >
<StatCard <StatCard
{...item} {...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> </div>
))} ))}
@ -219,12 +319,16 @@ export default function Analisa() {
<RevenueVsProfitChart <RevenueVsProfitChart
data={revenueVsProfit} data={revenueVsProfit}
title="Pendapatan vs Laba" title="Pendapatan vs Laba"
description={getFilterDescription("Analisis pendapatan kotor dibandingkan dengan laba bersih.")} description={getFilterDescription(
'Analisis pendapatan kotor dibandingkan dengan laba bersih.',
)}
/> />
<RevenueVsPurchasesChart <RevenueVsPurchasesChart
data={salesByMonth} data={salesByMonth}
title="Penjualan vs Belanja" 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> </div>
@ -233,16 +337,22 @@ export default function Analisa() {
<TransactionVolumeChart <TransactionVolumeChart
data={transactionVolume} data={transactionVolume}
title="Volume Transaksi" title="Volume Transaksi"
description={getFilterDescription("Total jumlah pesanan yang diproses per bulan.")} description={getFilterDescription(
'Total jumlah pesanan yang diproses per bulan.',
)}
/> />
<AovTrendChart <AovTrendChart
data={transactionVolume} data={transactionVolume}
title="Tren AOV" 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 <KnobChart
title="Repeat Order" title="Repeat Order"
description={getFilterDescription("Persentase pelanggan yang kembali berbelanja.")} description={getFilterDescription(
'Persentase pelanggan yang kembali berbelanja.',
)}
percentage={stats.repeat_order_percentage.value} percentage={stats.repeat_order_percentage.value}
label="Repeat Order" label="Repeat Order"
footerText={`${stats.repeat_customers.value} dari ${stats.total_customers.value} pelanggan melakukan 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 <SalesByHourChart
data={salesByHour} data={salesByHour}
title="Jam Sibuk" title="Jam Sibuk"
description={getFilterDescription("Grafik aktivitas transaksi berdasarkan waktu (jam) dalam sehari.")} description={getFilterDescription(
'Grafik aktivitas transaksi berdasarkan waktu (jam) dalam sehari.',
)}
config={{ config={{
total: { total: {
label: "Total Penjualan", label: 'Total Penjualan',
color: "var(--chart-1)", 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"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2 @4xl/main:grid-cols-4">
<CustomPieChart <CustomPieChart
title="Metode Pembayaran" title="Metode Pembayaran"
description={getFilterDescription("Distribusi transaksi berdasarkan metode pembayaran yang digunakan.")} description={getFilterDescription(
'Distribusi transaksi berdasarkan metode pembayaran yang digunakan.',
)}
data={paymentMethods} data={paymentMethods}
colorOffset={0} colorOffset={0}
/> />
<CustomPieChart <CustomPieChart
title="Status Pesanan" title="Status Pesanan"
description={getFilterDescription("Status terkini dari seluruh pesanan yang masuk.")} description={getFilterDescription(
'Status terkini dari seluruh pesanan yang masuk.',
)}
data={orderStatuses} data={orderStatuses}
colorOffset={120} colorOffset={120}
/> />
<CustomPieChart <CustomPieChart
title="Channel Pesanan" title="Channel Pesanan"
description={getFilterDescription("Sumber pesanan berdasarkan platform atau saluran penjualan.")} description={getFilterDescription(
'Sumber pesanan berdasarkan platform atau saluran penjualan.',
)}
data={orderChannels} data={orderChannels}
colorOffset={240} colorOffset={240}
/> />
<CustomPieChart <CustomPieChart
title="Top 5 Kategori" title="Top 5 Kategori"
description={getFilterDescription("Kategori produk yang paling banyak menyumbang penjualan.")} description={getFilterDescription(
'Kategori produk yang paling banyak menyumbang penjualan.',
)}
data={topCategories} data={topCategories}
colorOffset={60} colorOffset={60}
/> />
@ -291,13 +411,17 @@ export default function Analisa() {
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<CustomBarChart <CustomBarChart
title="Top 5 Produk" title="Top 5 Produk"
description={getFilterDescription("Daftar produk dengan volume penjualan tertinggi.")} description={getFilterDescription(
'Daftar produk dengan volume penjualan tertinggi.',
)}
data={topProducts} data={topProducts}
colorOffset={45} colorOffset={45}
/> />
<CustomBarChart <CustomBarChart
title="Top 5 Pelanggan" title="Top 5 Pelanggan"
description={getFilterDescription("Pelanggan dengan total nominal belanja tertinggi (setia).")} description={getFilterDescription(
'Pelanggan dengan total nominal belanja tertinggi (setia).',
)}
data={topCustomers} data={topCustomers}
colorOffset={180} colorOffset={180}
isCurrency={true} isCurrency={true}

View File

@ -77,12 +77,12 @@ return {
return ( return (
<> <>
<Head title="Dasbor" /> <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 */} {/* Welcome Section */}
<WelcomeCard user={auth.user} time={time} theme={theme} /> <WelcomeCard user={auth.user} time={time} theme={theme} />
{/* 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-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 Penjualan", stat: stats.total_sales, isCurrency: false },
{ title: "Total Pendapatan", stat: stats.total_revenue }, { title: "Total Pendapatan", stat: stats.total_revenue },
@ -104,7 +104,7 @@ return {
> >
<StatCard <StatCard
{...item} {...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> </div>
))} ))}
@ -116,7 +116,7 @@ return {
data={salesByHour} 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 <CustomPieChart
title="Metode Pembayaran" title="Metode Pembayaran"
description="Distribusi transaksi berdasarkan metode pembayaran yang digunakan." description="Distribusi transaksi berdasarkan metode pembayaran yang digunakan."
@ -137,7 +137,7 @@ return {
/> />
</div> </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 <CustomBarChart
title="Top 5 Produk" title="Top 5 Produk"
description="Daftar produk dengan volume penjualan tertinggi." description="Daftar produk dengan volume penjualan tertinggi."