update dashboard ux

This commit is contained in:
root
2026-06-24 03:04:01 -04:00
parent 2739f14e45
commit 572d115003
11 changed files with 449 additions and 36 deletions
+128 -24
View File
@@ -2,13 +2,25 @@
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { Calendar, Car, Users, DollarSign, AlertTriangle, Clock, XCircle, Plus, BarChart3, FileText, TrendingUp } from 'lucide-react'
import { Calendar, Car, Users, DollarSign, AlertTriangle, Clock, XCircle, Plus, BarChart3, FileText, TrendingUp, MapPin } from 'lucide-react'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'
import StatCard from '@/components/ui/StatCard'
import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
import { formatCurrency } from '@rentaldrivego/types'
/**
* Location breakdown item — requires backend field on /analytics/dashboard.
* Flagged as a contract change (not faked).
*/
interface LocationBreakdown {
name: string
bookingCount: number
revenue: number
/** 0100 utilization percentage */
utilizationPct: number
}
interface DashboardData {
kpis: {
totalBookings: number
@@ -19,6 +31,12 @@ interface DashboardData {
vehiclesChange: number
revenueChange: number
customersChange: number
/** Requires backend field — flagged as contract change */
utilizationRate?: number
/** Requires backend field — flagged as contract change */
revenuePerVehicle?: number
/** Requires backend field — flagged as contract change */
fulfillmentRate?: number
}
recentReservations: {
id: string
@@ -31,6 +49,8 @@ interface DashboardData {
totalAmount: number
}[]
sourceBreakdown: { source: string; count: number; revenue: number }[]
/** Requires backend field — flagged as contract change */
locationBreakdown?: LocationBreakdown[]
subscription: {
status: string
planName: string
@@ -132,6 +152,48 @@ function SubscriptionBanner({
)
}
function BranchPerformanceList({
locations,
formatCurrencyFn,
}: {
locations: LocationBreakdown[]
formatCurrencyFn: (amount: number, currency: string) => string
}) {
const maxCount = Math.max(...locations.map((l) => l.bookingCount), 1)
return (
<div className="glass-card p-6">
<div className="mb-4 flex items-center gap-2">
<MapPin className="h-4 w-4 text-orange-500" />
<h2 className="text-base font-semibold text-blue-950 dark:text-stone-100">Location Performance</h2>
</div>
<div className="space-y-4">
{locations.map((loc) => (
<div key={loc.name}>
<div className="mb-1 flex items-center justify-between">
<span className="text-sm font-medium text-blue-950 dark:text-stone-100">{loc.name}</span>
<span className="text-xs font-mono text-stone-500 dark:text-stone-400">
{loc.bookingCount} bookings · {formatCurrencyFn(loc.revenue, 'MAD')}
</span>
</div>
<div className="flex items-center gap-2">
<div className="progress-bar flex-1">
<div
className="progress-fill blue"
style={{ width: `${(loc.bookingCount / maxCount) * 100}%` }}
/>
</div>
<span className="w-8 text-right text-xs font-mono text-stone-500 dark:text-stone-400">
{loc.utilizationPct}%
</span>
</div>
</div>
))}
</div>
</div>
)
}
export default function DashboardPage() {
const { dict, language } = useDashboardI18n()
const d = dict.dashboardPage
@@ -251,6 +313,7 @@ export default function DashboardPage() {
icon={Calendar}
iconColor="text-blue-600"
iconBg="bg-blue-50"
pulse
/>
<StatCard
title={d.kpiActiveVehicles}
@@ -270,6 +333,7 @@ export default function DashboardPage() {
icon={DollarSign}
iconColor="text-orange-600"
iconBg="bg-orange-50"
valueGradient
/>
) : null}
<StatCard
@@ -283,32 +347,72 @@ export default function DashboardPage() {
/>
</div>
{/* New metrics row — contract change flag */}
{kpis.utilizationRate !== undefined && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<StatCard
title="Utilization Rate"
value={`${kpis.utilizationRate}%`}
icon={TrendingUp}
iconColor="text-blue-600"
iconBg="bg-blue-50"
progressPercent={kpis.utilizationRate}
target={100}
/>
<StatCard
title="Revenue / Vehicle"
value={formatCurrency(kpis.revenuePerVehicle ?? 0, 'MAD')}
icon={DollarSign}
iconColor="text-orange-600"
iconBg="bg-orange-50"
/>
<StatCard
title="Fulfillment Rate"
value={`${kpis.fulfillmentRate ?? 0}%`}
icon={BarChart3}
iconColor="text-green-600"
iconBg="bg-green-50"
progressPercent={kpis.fulfillmentRate ?? 0}
target={100}
/>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Booking Sources Chart */}
<div className="card p-6 lg:col-span-2">
<h2 className="text-base font-semibold text-slate-900 mb-4">{d.bookingSources}</h2>
{data?.sourceBreakdown && data.sourceBreakdown.length > 0 ? (
<ResponsiveContainer width="100%" height={240}>
<BarChart data={data.sourceBreakdown}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="source" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip
contentStyle={{ borderRadius: '8px', border: '1px solid #e2e8f0', fontSize: '12px' }}
/>
<Legend wrapperStyle={{ fontSize: '12px' }} />
<Bar dataKey="count" fill="#3b82f6" name={d.barBookings} radius={[4, 4, 0, 0]} />
<Bar dataKey="revenue" fill="#10b981" name={d.barRevenue} radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-48 flex items-center justify-center text-slate-400 text-sm">
{d.noBookingData}
</div>
)}
{/* Left column — 2/3: Booking Sources + Location Performance */}
<div className="lg:col-span-2 space-y-6">
<div className="card p-6">
<h2 className="text-base font-semibold text-slate-900 mb-4">{d.bookingSources}</h2>
{data?.sourceBreakdown && data.sourceBreakdown.length > 0 ? (
<ResponsiveContainer width="100%" height={240}>
<BarChart data={data.sourceBreakdown}>
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
<XAxis dataKey="source" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip
contentStyle={{ borderRadius: '8px', border: '1px solid #e2e8f0', fontSize: '12px' }}
/>
<Legend wrapperStyle={{ fontSize: '12px' }} />
<Bar dataKey="count" fill="#3b82f6" name={d.barBookings} radius={[4, 4, 0, 0]} />
<Bar dataKey="revenue" fill="#10b981" name={d.barRevenue} radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-48 flex items-center justify-center text-slate-400 text-sm">
{d.noBookingData}
</div>
)}
</div>
{data?.locationBreakdown && data.locationBreakdown.length > 0 ? (
<BranchPerformanceList
locations={data.locationBreakdown}
formatCurrencyFn={(a, c) => formatCurrency(a, c as 'MAD')}
/>
) : null}
</div>
{/* Quick stats */}
{/* Right column — 1/3: Quick Stats */}
<div className="card p-6">
<h2 className="text-base font-semibold text-slate-900 mb-4">{d.quickStats}</h2>
<div className="space-y-4">
+76
View File
@@ -17,6 +17,20 @@ html.dark {
--sidebar-bg: #0a1128;
--sidebar-text: #94a3b8;
--sidebar-active: #ffffff;
--bg-elevated: rgb(255 255 255 / 0.82);
--border-accent: rgb(231 229 228 / 0.8);
--glass-bg: rgb(255 255 255 / 0.72);
--shadow-card: 0 30px 80px rgba(28, 25, 23, 0.08);
--shadow-glow: 0 0 20px rgba(249, 115, 22, 0.15);
}
html.dark {
--bg-elevated: rgb(11 25 55 / 0.78);
--border-accent: rgb(30 60 140 / 0.40);
--glass-bg: rgb(11 25 55 / 0.65);
--shadow-card: 0 30px 80px rgba(0, 0, 0, 0.36);
--shadow-glow: 0 0 20px rgba(249, 115, 22, 0.25);
}
@layer base {
@@ -41,6 +55,68 @@ html.dark body {
}
@layer components {
.glass-card {
border-color: var(--border-accent);
background-color: var(--glass-bg);
box-shadow: var(--shadow-card);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
@apply rounded-[1.75rem] border transition-colors;
}
html.dark .glass-card {
border-color: rgb(30 60 140 / 0.40);
background-color: rgb(11 25 55 / 0.65);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.36);
}
.progress-bar {
@apply h-2 w-full overflow-hidden rounded-full;
background-color: rgb(231 229 228 / 0.5);
}
html.dark .progress-bar {
background-color: rgb(30 60 140 / 0.3);
}
.progress-fill {
@apply h-full rounded-full transition-all duration-500;
}
.progress-fill.blue {
@apply bg-gradient-to-r from-blue-500 to-blue-400;
}
.progress-fill.orange {
@apply bg-gradient-to-r from-orange-500 to-orange-400;
}
html.dark .progress-fill.blue {
@apply bg-gradient-to-r from-blue-400 to-blue-300;
}
html.dark .progress-fill.orange {
@apply bg-gradient-to-r from-orange-400 to-orange-300;
}
/* Pulse glow animation for live indicators */
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 4px rgba(249, 115, 22, 0.3); }
50% { box-shadow: 0 0 12px rgba(249, 115, 22, 0.6); }
}
.pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.pulse-glow,
.progress-fill {
animation: none !important;
transition: none !important;
}
}
.btn-primary {
@apply inline-flex items-center gap-2 rounded-full bg-orange-600 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-orange-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-orange-500 dark:hover:bg-orange-400;
}
+8 -1
View File
@@ -1,9 +1,16 @@
import type { Metadata } from 'next'
import { cookies } from 'next/headers'
import { JetBrains_Mono } from 'next/font/google'
import { DashboardI18nProvider } from '@/components/I18nProvider'
import { SHARED_LANGUAGE_COOKIE } from '@/lib/preferences'
import './globals.css'
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
display: 'swap',
})
export const metadata: Metadata = {
title: 'RentalDriveGo Dashboard',
description: 'Manage your rental car business',
@@ -30,7 +37,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
}}
/>
</head>
<body className="font-sans">
<body className={`font-sans ${jetbrainsMono.variable}`}>
<DashboardI18nProvider initialLanguage={initialLanguage}>{children}</DashboardI18nProvider>
</body>
</html>