From 803eebd0261d4bc0a086f5981cae2d304dbc701c Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 23 Apr 2026 10:14:12 +0700 Subject: [PATCH] feat: extract dashboard components and formatters to dedicated files for improved maintainability --- resources/js/components/cards/stat-card.tsx | 50 +++++ .../js/components/cards/welcome-card.tsx | 85 +++++++++ resources/js/lib/formatters.ts | 28 +++ resources/js/pages/dashboard.tsx | 171 ++---------------- 4 files changed, 179 insertions(+), 155 deletions(-) create mode 100644 resources/js/components/cards/stat-card.tsx create mode 100644 resources/js/components/cards/welcome-card.tsx create mode 100644 resources/js/lib/formatters.ts diff --git a/resources/js/components/cards/stat-card.tsx b/resources/js/components/cards/stat-card.tsx new file mode 100644 index 0000000..6b8e4c3 --- /dev/null +++ b/resources/js/components/cards/stat-card.tsx @@ -0,0 +1,50 @@ +import { cn } from '@/lib/utils'; +import { formatCurrency, formatNumber } from '@/lib/formatters'; +import { StatItem } from '@/types'; +import { TrendingUp, TrendingDown } from 'lucide-react'; +import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +export interface StatCardProps { + title: string; + stat: StatItem; + isCurrency?: boolean; + isPercentage?: boolean; + className?: string; +} + +export function StatCard({ + title, + stat, + isCurrency = true, + isPercentage = false, + className +}: StatCardProps) { + const isPositive = stat.change >= 0; + + return ( + + +
+ {title} +
+ + {isPercentage ? `${stat.value}%` : (isCurrency ? formatCurrency(stat.value) : formatNumber(stat.value))} + + + + {isPositive ? : } + {stat.change === null + ? '∞' + : `${Math.abs(stat.change)}%`} + + +
+ +
+ Kemarin: {isPercentage ? `${stat.yesterday}%` : (isCurrency ? formatCurrency(stat.yesterday) : formatNumber(stat.yesterday))} +
+
+
+ ); +} diff --git a/resources/js/components/cards/welcome-card.tsx b/resources/js/components/cards/welcome-card.tsx new file mode 100644 index 0000000..04ac820 --- /dev/null +++ b/resources/js/components/cards/welcome-card.tsx @@ -0,0 +1,85 @@ +import { formatTime, formatDate } from '@/lib/formatters'; +import { Sun, Moon, Cloud, Trees, Stars, Bird } from 'lucide-react'; + +const DynamicScene = ({ hour }: { hour: number }) => { + if (hour >= 5 && hour < 11) { + return ( +
+ + + +
+ ); + } + if (hour >= 11 && hour < 15) { + return ( +
+ + + +
+ ); + } + if (hour >= 15 && hour < 19) { + return ( +
+ + + +
+ ); + } + return ( +
+ + + +
+
+ ); +}; + +export interface WelcomeCardProps { + user: { name: string }; + time: Date; + theme: { + class: string; + decoration: string; + text: string; + subtext: string; + greeting: string; + }; +} + +export function WelcomeCard({ user, time, theme }: WelcomeCardProps) { + const hour = time.getHours(); + + return ( +
+ {/* Decorative Elements */} +
+
+ +
+

+ {theme.greeting}, {user.name}! +

+

+ Senang melihat Anda kembali. Berikut adalah ringkasan bisnis Anda hari ini. +

+
+ +
+ +
+
+ {formatTime(time)} +
+
+ {formatDate(time)} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/resources/js/lib/formatters.ts b/resources/js/lib/formatters.ts new file mode 100644 index 0000000..7b0cdc1 --- /dev/null +++ b/resources/js/lib/formatters.ts @@ -0,0 +1,28 @@ +export const formatTime = (date: Date) => { + return date.toLocaleTimeString('id-ID', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); +}; + +export const formatDate = (date: Date) => { + return date.toLocaleDateString('id-ID', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric', + }); +}; + +export const formatCurrency = (value: number) => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + }).format(value); +}; + +export const formatNumber = (value: number) => { + return new Intl.NumberFormat('id-ID').format(value); +}; diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index c3e571f..deac7f6 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,54 +1,10 @@ import { Head, usePage } from '@inertiajs/react'; import { useEffect, useState } from 'react'; -import { cn } from '@/lib/utils'; import { dashboard } from '@/routes'; -import { StatItem, DashboardPageProps } from '@/types'; -import { - Sun, Moon, Cloud, Trees, Stars, Bird, - TrendingUp, TrendingDown, ShoppingBag, DollarSign, - Wallet, Receipt, Percent, BarChart3, CreditCard, - ArrowUpRight, ArrowDownRight, Package, Users, ShoppingCart -} from 'lucide-react'; -import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardFooter } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; +import { DashboardPageProps } from '@/types'; +import { StatCard } from '@/components/cards/stat-card'; +import { WelcomeCard } from '@/components/cards/welcome-card'; -const DynamicScene = ({ hour }: { hour: number }) => { - if (hour >= 5 && hour < 11) { - return ( -
- - - -
- ); - } - if (hour >= 11 && hour < 15) { - return ( -
- - - -
- ); - } - if (hour >= 15 && hour < 19) { - return ( -
- - - -
- ); - } - return ( -
- - - -
-
- ); -}; export default function Dashboard() { const { auth, stats } = usePage().props; @@ -107,124 +63,29 @@ export default function Dashboard() { }; const theme = getTimeTheme(); - const hour = time.getHours(); - - const formattedTime = time.toLocaleTimeString('id-ID', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }); - - const formattedDate = time.toLocaleDateString('id-ID', { - weekday: 'long', - day: 'numeric', - month: 'long', - year: 'numeric', - }); - - const formatCurrency = (value: number) => { - return new Intl.NumberFormat('id-ID', { - style: 'currency', - currency: 'IDR', - minimumFractionDigits: 0, - }).format(value); - }; - - const formatNumber = (value: number) => { - return new Intl.NumberFormat('id-ID').format(value); - }; - - const StatCard = ({ - title, - stat, - icon: Icon, - isCurrency = true, - isPercentage = false, - className - }: { - title: string; - stat: StatItem; - icon: any; - isCurrency?: boolean; - isPercentage?: boolean; - className?: string; - }) => { - const isPositive = stat.change >= 0; - - return ( - - -
- {title} -
- - {isPercentage ? `${stat.value}%` : (isCurrency ? formatCurrency(stat.value) : formatNumber(stat.value))} - - - - {isPositive ? : } - {stat.change === null - ? '∞' - : `${Math.abs(stat.change)}%`} - - -
- -
- Kemarin: {isPercentage ? `${stat.yesterday}%` : (isCurrency ? formatCurrency(stat.yesterday) : formatNumber(stat.yesterday))} -
-
-
- ); - }; return ( <>
{/* Welcome Section */} -
- {/* Decorative Elements */} -
-
- -
-

- {theme.greeting}, {auth.user.name}! -

-

- Senang melihat Anda kembali. Berikut adalah ringkasan bisnis Anda hari ini. -

-
- -
- -
-
- {formattedTime} -
-
- {formattedDate} -
-
-
-
+ {/* Stats Grid */}
{[ - { title: "Total Penjualan", stat: stats.total_sales, icon: ShoppingBag, isCurrency: false }, - { title: "Total Pendapatan", stat: stats.total_revenue, icon: DollarSign }, - { title: "Total Pengeluaran", stat: stats.total_expenses, icon: Wallet }, - { title: "HPP (Modal)", stat: stats.cogs, icon: Receipt }, - { title: "AOV (Avg Order Value)", stat: stats.aov, icon: CreditCard }, - { title: "Profit Margin", stat: stats.profit_margin, icon: Percent, isPercentage: true }, - { title: "Total Diskon", stat: stats.total_discount, icon: Percent }, - { title: "Laba Kotor", stat: stats.gross_profit, icon: BarChart3 }, - { title: "Laba Bersih", stat: stats.net_profit, icon: TrendingUp }, - { title: "Total Pembelian", stat: stats.total_purchases, icon: ShoppingCart }, - { title: "Produk Terjual", stat: stats.products_sold, icon: Package, isCurrency: false }, - { title: "Total Pelanggan", stat: stats.total_customers, icon: Users, 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 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) => (