diff --git a/apps/dashboard/src/app/(dashboard)/page.tsx b/apps/dashboard/src/app/(dashboard)/page.tsx index ac4bc3a..d9a6466 100644 --- a/apps/dashboard/src/app/(dashboard)/page.tsx +++ b/apps/dashboard/src/app/(dashboard)/page.tsx @@ -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 + /** 0–100 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 ( +
+
+ +

Location Performance

+
+
+ {locations.map((loc) => ( +
+
+ {loc.name} + + {loc.bookingCount} bookings · {formatCurrencyFn(loc.revenue, 'MAD')} + +
+
+
+
+
+ + {loc.utilizationPct}% + +
+
+ ))} +
+
+ ) +} + 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 /> ) : null}
+ {/* New metrics row — contract change flag */} + {kpis.utilizationRate !== undefined && ( +
+ + + +
+ )} +
- {/* Booking Sources Chart */} -
-

{d.bookingSources}

- {data?.sourceBreakdown && data.sourceBreakdown.length > 0 ? ( - - - - - - - - - - - - ) : ( -
- {d.noBookingData} -
- )} + {/* Left column — 2/3: Booking Sources + Location Performance */} +
+
+

{d.bookingSources}

+ {data?.sourceBreakdown && data.sourceBreakdown.length > 0 ? ( + + + + + + + + + + + + ) : ( +
+ {d.noBookingData} +
+ )} +
+ + {data?.locationBreakdown && data.locationBreakdown.length > 0 ? ( + formatCurrency(a, c as 'MAD')} + /> + ) : null}
- {/* Quick stats */} + {/* Right column — 1/3: Quick Stats */}

{d.quickStats}

diff --git a/apps/dashboard/src/app/globals.css b/apps/dashboard/src/app/globals.css index 53aa036..545e1b6 100644 --- a/apps/dashboard/src/app/globals.css +++ b/apps/dashboard/src/app/globals.css @@ -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; } diff --git a/apps/dashboard/src/app/layout.tsx b/apps/dashboard/src/app/layout.tsx index 18be8f0..3fd5eb7 100644 --- a/apps/dashboard/src/app/layout.tsx +++ b/apps/dashboard/src/app/layout.tsx @@ -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 }} /> - + {children} diff --git a/apps/dashboard/src/components/layout/Sidebar.tsx b/apps/dashboard/src/components/layout/Sidebar.tsx index 6f76b4f..a790570 100644 --- a/apps/dashboard/src/components/layout/Sidebar.tsx +++ b/apps/dashboard/src/components/layout/Sidebar.tsx @@ -303,7 +303,9 @@ export default function Sidebar() { const className = [ 'flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-medium transition-colors', paddingClass, - active ? 'bg-orange-500 text-white' : 'text-slate-400 hover:bg-blue-900/40 hover:text-white', + active + ? 'bg-orange-500 text-white shadow-[0_0_12px_rgba(249,115,22,0.3)] dark:shadow-[0_0_16px_rgba(249,115,22,0.35)]' + : 'text-slate-400 hover:bg-blue-900/40 hover:text-white', ].join(' ') if (item.itemType === 'EXTERNAL_LINK' && item.routeOrUrl) { @@ -364,22 +366,31 @@ export default function Sidebar() {