This commit is contained in:
2026-04-01 15:34:50 +03:00
parent 13b56d4960
commit 5f3d208158
120 changed files with 36338 additions and 21265 deletions
@@ -0,0 +1,68 @@
"use client"
import { CalendarCheck, CalendarX, Eye, Ban } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import type { DashboardData } from "./use-dashboard-data"
type Props = { data: DashboardData }
export function AppointmentsSummaryCard({ data }: Props) {
const totals = data.appointments_summary?.totals
const stats = [
{
label: "Completed",
value: totals?.completed?.text ?? "0 Appt.",
icon: CalendarCheck,
color: "text-emerald-600",
bg: "bg-emerald-500/10",
},
{
label: "No Shows",
value: totals?.no_shows?.text ?? "0 Appt.",
icon: Eye,
color: "text-amber-600",
bg: "bg-amber-500/10",
},
{
label: "No-Show Rate",
value: totals?.no_shows_rate?.text ?? "0%",
icon: Ban,
color: "text-red-600",
bg: "bg-red-500/10",
},
{
label: "Cancelled",
value: totals?.cancelled?.text ?? "0 Appt.",
icon: CalendarX,
color: "text-slate-600",
bg: "bg-slate-500/10",
},
]
return (
<Card>
<CardHeader>
<CardTitle>Appointments Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
{stats.map((stat) => (
<div
key={stat.label}
className="flex items-center gap-3 rounded-lg border p-3"
>
<div className={`rounded-md p-2 ${stat.bg}`}>
<stat.icon className={`h-4 w-4 ${stat.color}`} />
</div>
<div>
<p className="text-sm font-medium">{stat.value}</p>
<p className="text-xs text-muted-foreground">{stat.label}</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,40 @@
"use client"
import { Users, Building2, Truck, Shield } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import type { DashboardData } from "./use-dashboard-data"
type Props = { data: DashboardData }
export function CustomersTotalsCard({ data }: Props) {
const customers = data.customers_totals
const stats = [
{ label: "Individuals", value: customers?.individuals ?? 0, icon: Users, color: "text-sky-600", bg: "bg-sky-500/10" },
{ label: "Companies", value: customers?.companies ?? 0, icon: Building2, color: "text-indigo-600", bg: "bg-indigo-500/10" },
{ label: "Fleets", value: customers?.fleets ?? 0, icon: Truck, color: "text-teal-600", bg: "bg-teal-500/10" },
{ label: "Insurers", value: customers?.insurers ?? 0, icon: Shield, color: "text-rose-600", bg: "bg-rose-500/10" },
]
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">Customers</CardTitle>
<span className="text-2xl font-bold">{customers?.total_customers ?? 0}</span>
</CardHeader>
<CardContent className="space-y-3">
{stats.map((stat) => (
<div key={stat.label} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`rounded-md p-1.5 ${stat.bg}`}>
<stat.icon className={`h-3.5 w-3.5 ${stat.color}`} />
</div>
<span className="text-sm text-muted-foreground">{stat.label}</span>
</div>
<span className="text-sm font-semibold">{stat.value}</span>
</div>
))}
</CardContent>
</Card>
)
}
@@ -0,0 +1,69 @@
"use client"
import { Loader2 } from "lucide-react"
import { useDashboardData } from "./use-dashboard-data"
import { FinancialTotalsCards } from "./financial-totals-cards"
import { IncomeExpenseChart } from "./income-expense-chart"
import { FinancialSummaryChart } from "./financial-summary-chart"
import { WorkOrdersStatusCard } from "./work-orders-status-card"
import { AppointmentsSummaryCard } from "./appointments-summary-card"
import { UpcomingAppointmentsCard } from "./upcoming-appointments-card"
import { ItemsTotalsCard } from "./items-totals-card"
import { CustomersTotalsCard } from "./customers-totals-card"
import { SalesPurchaseCards } from "./sales-purchase-cards"
import { VehicleStatsCards } from "./vehicle-stats-cards"
export function DashboardContent() {
const { data, isLoading, isError, error } = useDashboardData()
if (isLoading) {
return (
<div className="flex items-center justify-center py-24">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)
}
if (isError || !data) {
return (
<div className="flex flex-col items-center justify-center py-24 text-muted-foreground">
<p className="text-lg font-medium">Failed to load dashboard</p>
<p className="text-sm">{error?.message ?? "An unexpected error occurred"}</p>
</div>
)
}
return (
<div className="space-y-6">
{/* Financial Overview */}
<FinancialTotalsCards data={data} />
{/* Charts Row */}
<div className="grid gap-4 lg:grid-cols-2">
<IncomeExpenseChart data={data} />
<FinancialSummaryChart data={data} />
</div>
{/* Work Orders + Appointments */}
<div className="grid gap-4 lg:grid-cols-2">
<WorkOrdersStatusCard data={data} />
<AppointmentsSummaryCard data={data} />
</div>
{/* Upcoming Appointments */}
<UpcomingAppointmentsCard data={data} />
{/* Sales & Purchase Documents */}
<SalesPurchaseCards data={data} />
{/* Quick Stats Row */}
<div className="grid gap-4 md:grid-cols-2">
<ItemsTotalsCard data={data} />
<CustomersTotalsCard data={data} />
</div>
{/* Vehicle Statistics */}
<VehicleStatsCards data={data} />
</div>
)
}
@@ -0,0 +1,75 @@
"use client"
import { Bar, BarChart, XAxis, YAxis, CartesianGrid } from "recharts"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/shared/components/ui/chart"
import type { DashboardData } from "./use-dashboard-data"
const chartConfig = {
amount: {
label: "Amount",
color: "var(--color-blue-500, #3b82f6)",
},
} satisfies ChartConfig
type Props = { data: DashboardData }
export function FinancialSummaryChart({ data }: Props) {
const summary = data.financial_summary
const currency = summary?.currency ?? ""
const chartData = (summary?.chart ?? []).map((item) => ({
label: item.label ?? "",
amount: item.amount ?? 0,
count: item.count ?? 0,
}))
const colors = ["#3b82f6", "#f59e0b", "#ef4444"]
return (
<Card>
<CardHeader>
<CardTitle>Financial Summary</CardTitle>
<CardDescription>
Invoices, expenses & bills breakdown ({currency})
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-75 w-full">
<BarChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="label" tickLine={false} axisLine={false} />
<YAxis tickLine={false} axisLine={false} tickFormatter={(v: number) => v.toLocaleString()} />
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value, _name, item) => (
<div className="flex flex-col">
<span className="font-medium">{currency} {Number(value).toLocaleString()}</span>
<span className="text-muted-foreground">{item.payload.count} document(s)</span>
</div>
)}
/>
}
/>
<Bar
dataKey="amount"
radius={[6, 6, 0, 0]}
fill="var(--color-amount)"
barSize={48}
>
{chartData.map((_entry, index) => (
<rect key={index} fill={colors[index % colors.length]} />
))}
</Bar>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
)
}
@@ -0,0 +1,63 @@
"use client"
import {
DollarSign,
TrendingUp,
TrendingDown,
ArrowUpRight,
ArrowDownRight,
} from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import type { DashboardData } from "./use-dashboard-data"
type Props = { data: DashboardData }
export function FinancialTotalsCards({ data }: Props) {
const totals = data.totals
return (
<div className="grid gap-4 md:grid-cols-2">
<Card className="relative overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Income
</CardTitle>
<div className="rounded-md bg-emerald-500/10 p-2">
<TrendingUp className="h-4 w-4 text-emerald-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-emerald-600">
{totals?.total_income_text ?? `${totals?.currency ?? ""} 0.00`}
</div>
<div className="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
<ArrowUpRight className="h-3 w-3 text-emerald-500" />
Income this period
</div>
</CardContent>
<div className="absolute -bottom-4 -right-4 h-24 w-24 rounded-full bg-emerald-500/5" />
</Card>
<Card className="relative overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Expenses
</CardTitle>
<div className="rounded-md bg-red-500/10 p-2">
<TrendingDown className="h-4 w-4 text-red-600" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-600">
{totals?.total_expense_text ?? `${totals?.currency ?? ""} 0.00`}
</div>
<div className="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
<ArrowDownRight className="h-3 w-3 text-red-500" />
Expenses this period
</div>
</CardContent>
<div className="absolute -bottom-4 -right-4 h-24 w-24 rounded-full bg-red-500/5" />
</Card>
</div>
)
}
@@ -0,0 +1,106 @@
"use client"
import {
Area,
AreaChart,
CartesianGrid,
XAxis,
YAxis,
} from "recharts"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/shared/components/ui/chart"
import type { DashboardData } from "./use-dashboard-data"
const chartConfig = {
income: {
label: "Income",
color: "var(--color-emerald-500, #10b981)",
},
expense: {
label: "Expense",
color: "var(--color-red-500, #ef4444)",
},
} satisfies ChartConfig
type Props = { data: DashboardData }
export function IncomeExpenseChart({ data }: Props) {
const series = data.chart?.series ?? []
const currency = data.chart?.currency ?? ""
const chartData = series.map((item) => ({
date: item.date ?? "",
income: item.income ?? 0,
expense: item.expense ?? 0,
}))
return (
<Card>
<CardHeader>
<CardTitle>Income vs Expenses</CardTitle>
<CardDescription>
Financial trend over the selected period ({currency})
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={chartConfig} className="h-75 w-full">
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="fillIncome" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-income)" stopOpacity={0.3} />
<stop offset="95%" stopColor="var(--color-income)" stopOpacity={0.05} />
</linearGradient>
<linearGradient id="fillExpense" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-expense)" stopOpacity={0.3} />
<stop offset="95%" stopColor="var(--color-expense)" stopOpacity={0.05} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickFormatter={(value: string) => {
const date = new Date(value)
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" })
}}
/>
<YAxis tickLine={false} axisLine={false} tickFormatter={(v: number) => `${v.toLocaleString()}`} />
<ChartTooltip
content={
<ChartTooltipContent
labelFormatter={(value) => {
return new Date(value).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
}}
/>
}
/>
<Area
type="monotone"
dataKey="income"
stroke="var(--color-income)"
fill="url(#fillIncome)"
strokeWidth={2}
/>
<Area
type="monotone"
dataKey="expense"
stroke="var(--color-expense)"
fill="url(#fillExpense)"
strokeWidth={2}
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
)
}
@@ -0,0 +1,39 @@
"use client"
import { Package, Wrench, Layers } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import type { DashboardData } from "./use-dashboard-data"
type Props = { data: DashboardData }
export function ItemsTotalsCard({ data }: Props) {
const items = data.items_totals
const stats = [
{ label: "Parts", value: items?.parts ?? 0, icon: Package, color: "text-blue-600", bg: "bg-blue-500/10" },
{ label: "Services", value: items?.services ?? 0, icon: Wrench, color: "text-violet-600", bg: "bg-violet-500/10" },
{ label: "Service Groups", value: items?.service_groups ?? 0, icon: Layers, color: "text-amber-600", bg: "bg-amber-500/10" },
]
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">Items</CardTitle>
<span className="text-2xl font-bold">{items?.total_items ?? 0}</span>
</CardHeader>
<CardContent className="space-y-3">
{stats.map((stat) => (
<div key={stat.label} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`rounded-md p-1.5 ${stat.bg}`}>
<stat.icon className={`h-3.5 w-3.5 ${stat.color}`} />
</div>
<span className="text-sm text-muted-foreground">{stat.label}</span>
</div>
<span className="text-sm font-semibold">{stat.value}</span>
</div>
))}
</CardContent>
</Card>
)
}
@@ -0,0 +1,78 @@
"use client"
import { FileText, FileSearch, Receipt, ShoppingCart } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import type { DashboardData } from "./use-dashboard-data"
type Props = { data: DashboardData }
export function SalesPurchaseCards({ data }: Props) {
const sales = data.sales_totals
const purchase = data.purchase_totals
const salesStats = [
{ label: "Inspections", value: sales?.inspections ?? 0, icon: FileSearch },
{ label: "Estimates", value: sales?.estimates ?? 0, icon: FileText },
{ label: "Invoices", value: sales?.invoices ?? 0, icon: Receipt },
]
const purchaseStats = [
{ label: "Purchase Orders", value: purchase?.purchase_orders ?? 0, icon: ShoppingCart },
{ label: "Bills", value: purchase?.bills ?? 0, icon: Receipt },
{ label: "Expenses", value: purchase?.expenses ?? 0, icon: FileText },
]
return (
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">Sales Documents</CardTitle>
<span className="text-2xl font-bold text-emerald-600">
{sales?.total_sales_documents ?? 0}
</span>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-2">
{salesStats.map((stat) => (
<div
key={stat.label}
className="flex flex-col items-center gap-1 rounded-lg border p-3 text-center"
>
<stat.icon className="h-4 w-4 text-muted-foreground" />
<span className="text-lg font-bold">{stat.value}</span>
<span className="text-[11px] text-muted-foreground leading-tight">
{stat.label}
</span>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-sm font-medium">Purchase Documents</CardTitle>
<span className="text-2xl font-bold text-blue-600">
{purchase?.total_purchase_documents ?? 0}
</span>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-2">
{purchaseStats.map((stat) => (
<div
key={stat.label}
className="flex flex-col items-center gap-1 rounded-lg border p-3 text-center"
>
<stat.icon className="h-4 w-4 text-muted-foreground" />
<span className="text-lg font-bold">{stat.value}</span>
<span className="text-[11px] text-muted-foreground leading-tight">
{stat.label}
</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,98 @@
"use client"
import { Calendar, Clock } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Badge } from "@/shared/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/components/ui/tabs"
import type { DashboardData } from "./use-dashboard-data"
type Props = { data: DashboardData }
const statusBadge: Record<string, string> = {
confirmed: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300",
pending: "bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300",
cancelled: "bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300",
}
type AppointmentDetail = NonNullable<NonNullable<NonNullable<DashboardData["upcoming_appointments"]>["today"]>["details"]>[number]
function AppointmentRow({ appt }: { appt: AppointmentDetail }) {
return (
<div className="flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-muted/50">
<div className="flex items-center gap-3">
<div className="rounded-md bg-primary/10 p-2">
<Calendar className="h-4 w-4 text-primary" />
</div>
<div>
<p className="text-sm font-medium">{appt.title}</p>
{appt.notes && (
<p className="text-xs text-muted-foreground line-clamp-1">{appt.notes}</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
{appt.from_time?.slice(0, 5)} - {appt.to_time?.slice(0, 5)}
</div>
<Badge variant="secondary" className={statusBadge[appt.status ?? ""] ?? ""}>
{appt.status}
</Badge>
</div>
</div>
)
}
function EmptyState() {
return (
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
<Calendar className="h-8 w-8 mb-2 opacity-50" />
<p className="text-sm">No appointments</p>
</div>
)
}
export function UpcomingAppointmentsCard({ data }: Props) {
const upcoming = data.upcoming_appointments
const tabs = [
{ key: "today", label: "Today", data: upcoming?.today },
{ key: "tomorrow", label: "Tomorrow", data: upcoming?.tomorrow },
{ key: "this_week", label: "This Week", data: upcoming?.this_week },
{ key: "next_week", label: "Next Week", data: upcoming?.next_week },
]
return (
<Card>
<CardHeader>
<CardTitle>Upcoming Appointments</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="today">
<TabsList className="w-full">
{tabs.map((tab) => (
<TabsTrigger key={tab.key} value={tab.key} className="flex-1 text-xs">
{tab.label}
<Badge variant="secondary" className="ml-1.5 h-5 px-1.5 text-[10px]">
{tab.data?.count ?? 0}
</Badge>
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => (
<TabsContent key={tab.key} value={tab.key} className="space-y-2 mt-3">
{(tab.data?.details as AppointmentDetail[] | undefined)?.length ? (
(tab.data?.details as AppointmentDetail[]).map((appt) => (
<AppointmentRow key={appt.id} appt={appt} />
))
) : (
<EmptyState />
)}
</TabsContent>
))}
</Tabs>
</CardContent>
</Card>
)
}
@@ -0,0 +1,16 @@
"use client"
import { useQuery } from "@tanstack/react-query"
import { useAuthApi } from "@/shared/useApi"
import type { HomeDashboardResponse } from "@garage/api"
export type DashboardData = HomeDashboardResponse
export function useDashboardData() {
const api = useAuthApi()
return useQuery<DashboardData>({
queryKey: ["home", "dashboard"],
queryFn: () => api.home.dashboard(),
})
}
@@ -0,0 +1,104 @@
"use client"
import { Car } from "lucide-react"
import { Bar, BarChart, XAxis, YAxis, CartesianGrid, Cell } from "recharts"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/ui/card"
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/shared/components/ui/chart"
import type { DashboardData } from "./use-dashboard-data"
const chartConfig = {
vehicles_count: {
label: "Vehicles",
color: "var(--color-blue-500, #3b82f6)",
},
} satisfies ChartConfig
const COLORS = ["#3b82f6", "#8b5cf6", "#06b6d4", "#f59e0b", "#ef4444", "#10b981", "#ec4899", "#6366f1"]
type Props = { data: DashboardData }
export function VehicleStatsCards({ data }: Props) {
const bodyTypes = data.body_types_vehicle_totals ?? []
const makes = data.make_model_vehicle_totals?.makes ?? []
const bodyData = bodyTypes.map((bt) => ({
name: bt.body_type ?? "Unknown",
vehicles_count: bt.vehicles_count ?? 0,
}))
const makeData = makes.map((m) => ({
name: m.make ?? "Unknown",
vehicles_count: m.vehicles_count ?? 0,
}))
return (
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Car className="h-5 w-5 text-muted-foreground" />
<div>
<CardTitle>Vehicles by Body Type</CardTitle>
<CardDescription>Distribution across body types</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{bodyData.length > 0 ? (
<ChartContainer config={chartConfig} className="h-55 w-full">
<BarChart data={bodyData} layout="vertical" margin={{ top: 0, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" horizontal={false} />
<XAxis type="number" tickLine={false} axisLine={false} />
<YAxis type="category" dataKey="name" tickLine={false} axisLine={false} width={80} />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="vehicles_count" radius={[0, 6, 6, 0]} barSize={24}>
{bodyData.map((_entry, index) => (
<Cell key={index} fill={COLORS[index % COLORS.length]} />
))}
</Bar>
</BarChart>
</ChartContainer>
) : (
<p className="text-sm text-muted-foreground text-center py-8">No vehicle data</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Car className="h-5 w-5 text-muted-foreground" />
<div>
<CardTitle>Vehicles by Make</CardTitle>
<CardDescription>Top vehicle manufacturers</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
{makeData.length > 0 ? (
<ChartContainer config={chartConfig} className="h-55 w-full">
<BarChart data={makeData} layout="vertical" margin={{ top: 0, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" horizontal={false} />
<XAxis type="number" tickLine={false} axisLine={false} />
<YAxis type="category" dataKey="name" tickLine={false} axisLine={false} width={80} />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="vehicles_count" radius={[0, 6, 6, 0]} barSize={24}>
{makeData.map((_entry, index) => (
<Cell key={index} fill={COLORS[index % COLORS.length]} />
))}
</Bar>
</BarChart>
</ChartContainer>
) : (
<p className="text-sm text-muted-foreground text-center py-8">No vehicle data</p>
)}
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,69 @@
"use client"
import { ClipboardList } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Badge } from "@/shared/components/ui/badge"
import { Progress } from "@/shared/components/ui/progress"
import type { DashboardData } from "./use-dashboard-data"
type Props = { data: DashboardData }
const statusColors: Record<string, string> = {
draft: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300",
check_in: "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300",
in_progress: "bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300",
completed: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300",
cancelled: "bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300",
invoiced: "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300",
}
export function WorkOrdersStatusCard({ data }: Props) {
const workOrders = data.work_orders_status
const cards = workOrders?.cards ?? []
const totals = workOrders?.totals
const totalOrders = totals?.orders ?? 0
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div className="flex items-center gap-2">
<ClipboardList className="h-5 w-5 text-muted-foreground" />
<CardTitle>Work Orders</CardTitle>
</div>
<div className="text-right">
<p className="text-2xl font-bold">{totals?.orders_text ?? "0 Orders"}</p>
<p className="text-xs text-muted-foreground">{totals?.amount_text ?? ""}</p>
</div>
</CardHeader>
<CardContent className="space-y-3">
{cards.map((card) => {
const percentage = totalOrders > 0 ? ((card.count ?? 0) / totalOrders) * 100 : 0
return (
<div key={card.status} className="space-y-1.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge
variant="secondary"
className={statusColors[card.status ?? ""] ?? ""}
>
{card.label}
</Badge>
<span className="text-sm text-muted-foreground">
{card.orders_text}
</span>
</div>
<span className="text-sm font-medium">{card.amount_text}</span>
</div>
<Progress value={percentage} className="h-1.5" />
</div>
)
})}
{cards.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">
No work orders this period
</p>
)}
</CardContent>
</Card>
)
}