chore: bulk commit of pre-existing changes
Build & Deploy / Build & Push Docker Image (push) Successful in 48s
Test / API Unit Tests (push) Successful in 9m48s
Test / Marketplace Unit Tests (push) Successful in 9m38s
Test / Admin Unit Tests (push) Successful in 9m33s
Build & Deploy / Deploy to VPS (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled

Includes:
- Admin dashboard layout and page updates
- API account auth module (routes, schemas, service)
- Dashboard sign-up form extraction, middleware refactor
- Marketplace proxy and middleware updates
- CORS origins expansion for dev environment
- Seed email domain correction (.com → .ma)
- Docs: create-account guide, fleet page, signup plan
- Various UI component and layout refinements
This commit is contained in:
root
2026-06-25 00:57:59 -04:00
parent 572d115003
commit 3b142ca4c7
36 changed files with 2987 additions and 1156 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ export default function AdminDashboardLayout({ children }: { children: React.Rea
<aside className="flex w-60 flex-shrink-0 flex-col border-r border-stone-200/80 bg-white/78 backdrop-blur-xl transition-colors dark:border-blue-900 dark:bg-[#07101e]/78">
<Link href="/" className="block px-5 py-6">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-orange-700 dark:text-orange-300">{dict.admin}</p>
<p className="mt-0.5 text-sm font-semibold text-blue-950 dark:text-stone-100">RentalDriveGo</p>
<p className="mt-0.5 text-sm font-semibold text-blue-950 dark:text-stone-100">FleetOS</p>
</Link>
<nav className="flex-1 px-3 space-y-0.5">
{navLinks.map((link) => {
+161 -39
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from 'react'
import Link from 'next/link'
import { Activity, ArrowRight, Building2, ClipboardList, ShieldCheck, Users, WalletCards } from 'lucide-react'
import { useAdminI18n } from '@/components/I18nProvider'
import { ADMIN_API_BASE } from '@/lib/api'
import { canAccessAdminMetrics, useAdminSession } from './AdminSessionContext'
@@ -15,35 +16,65 @@ interface Metrics {
}
export default function AdminDashboardPage() {
const { language, dict } = useAdminI18n()
const { language } = useAdminI18n()
const admin = useAdminSession()
const copy = {
en: {
kpis: ['Total companies', 'Active companies', 'Total renters', 'Total reservations'],
platformOverview: 'Platform overview',
brand: 'FleetOS',
eyebrow: 'Operations command',
platformOverview: 'FleetOS admin dashboard',
subtitle: 'Monitor marketplace health, subscription coverage, renter activity, and operator actions from one control surface.',
liveSignal: 'Live platform signal',
readiness: 'Operational readiness',
readinessBody: 'Core admin workflows are connected and ready for platform support.',
quickActions: 'Quick actions',
mrr: 'Monthly recurring revenue',
noMetrics: 'Metrics are limited for this admin role.',
cards: [
['Companies', 'List, search, suspend, reactivate, and review subscription state.', 'View all'],
['Renters', 'Support flows for blocking and unblocking marketplace renters.', 'View all'],
['Audit logs', 'Full trace of every admin action taken on the platform.', 'View logs'],
['Companies', 'Search operators, review subscription state, and manage account status.', 'View all'],
['Renters', 'Support renter trust workflows, blocking, and account recovery.', 'View all'],
['Audit logs', 'Trace every sensitive admin action across FleetOS.', 'View logs'],
],
health: ['Tenant accounts', 'Marketplace identity', 'Admin audit trail'],
},
fr: {
kpis: ['Entreprises totales', 'Entreprises actives', 'Locataires totaux', 'Réservations totales'],
platformOverview: 'Vue densemble de la plateforme',
brand: 'FleetOS',
eyebrow: 'Centre des opérations',
platformOverview: 'Tableau de bord admin FleetOS',
subtitle: 'Suivez la santé de la marketplace, les abonnements, lactivité des locataires et les actions opérateur depuis une même interface.',
liveSignal: 'Signal plateforme en direct',
readiness: 'Disponibilité opérationnelle',
readinessBody: 'Les principaux workflows admin sont connectés et prêts pour le support plateforme.',
quickActions: 'Actions rapides',
mrr: 'Revenu mensuel récurrent',
noMetrics: 'Les métriques sont limitées pour ce rôle admin.',
cards: [
['Entreprises', 'Lister, rechercher, suspendre, réactiver et consulter l’état des abonnements.', 'Voir tout'],
['Locataires', 'Gérer lassistance liée au blocage et au déblocage des locataires de la marketplace.', 'Voir tout'],
['Journaux daudit', 'Consulter la trace complète de chaque action dadministration sur la plateforme.', 'Voir les journaux'],
['Entreprises', 'Rechercher les opérateurs, vérifier les abonnements et gérer les statuts.', 'Voir tout'],
['Locataires', 'Gérer les workflows de confiance, de blocage et de récupération.', 'Voir tout'],
['Journaux daudit', 'Tracer chaque action admin sensible dans FleetOS.', 'Voir les journaux'],
],
health: ['Comptes locataires', 'Identité marketplace', 'Piste daudit admin'],
},
ar: {
kpis: ['إجمالي الشركات', 'الشركات النشطة', 'إجمالي المستأجرين', 'إجمالي الحجوزات'],
platformOverview: 'نظرة عامة على المنصة',
brand: 'FleetOS',
eyebrow: 'مركز العمليات',
platformOverview: 'لوحة إدارة FleetOS',
subtitle: 'راقب صحة السوق وحالة الاشتراكات ونشاط المستأجرين وإجراءات الإدارة من مساحة واحدة.',
liveSignal: 'مؤشر المنصة المباشر',
readiness: 'جاهزية التشغيل',
readinessBody: 'مسارات الإدارة الأساسية متصلة وجاهزة لدعم المنصة.',
quickActions: 'إجراءات سريعة',
mrr: 'الإيراد الشهري المتكرر',
noMetrics: 'المؤشرات محدودة لهذا الدور الإداري.',
cards: [
['الشركات', 'اعرض الشركات وابحث عنها وعلّقها وأعد تفعيلها وراجع حالة الاشتراك.', 'عرض الكل'],
['المستأجرون', 'إدارة طلبات الدعم الخاصة بحظر مستأجري السوق وفك الحظر عنهم.', 'عرض الكل'],
['سجلات التدقيق', 'مراجعة السجل الكامل لكل إجراء إداري تم على المنصة.', 'عرض السجلات'],
['الشركات', 'ابحث عن المشغلين وراجع الاشتراكات وأدر حالة الحساب.', 'عرض الكل'],
['المستأجرون', 'إدارة الثقة والحظر واستعادة الحسابات للمستأجرين.', 'عرض الكل'],
['سجلات التدقيق', 'تتبع كل إجراء إداري حساس داخل FleetOS.', 'عرض السجلات'],
],
health: ['حسابات الشركات', 'هوية السوق', 'سجل تدقيق الإدارة'],
},
}[language]
const [metrics, setMetrics] = useState<Metrics | null>(null)
@@ -72,21 +103,90 @@ export default function AdminDashboardPage() {
.finally(() => setLoading(false))
}, [admin])
const numberFormatter = new Intl.NumberFormat(language === 'ar' ? 'ar-MA' : language === 'fr' ? 'fr-FR' : 'en-US')
const currencyFormatter = new Intl.NumberFormat(language === 'ar' ? 'ar-MA' : language === 'fr' ? 'fr-FR' : 'en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
})
const kpis = metrics
? [
{ label: 'Total companies', value: metrics.totalCompanies },
{ label: copy.kpis[1], value: metrics.activeCompanies },
{ label: copy.kpis[2], value: metrics.totalRenters },
{ label: copy.kpis[3], value: metrics.totalReservations },
{ label: copy.kpis[0], value: metrics.totalCompanies, icon: Building2, tone: 'text-blue-600 dark:text-blue-300', bg: 'bg-blue-50 dark:bg-blue-500/10' },
{ label: copy.kpis[1], value: metrics.activeCompanies, icon: Activity, tone: 'text-emerald-600 dark:text-emerald-300', bg: 'bg-emerald-50 dark:bg-emerald-500/10' },
{ label: copy.kpis[2], value: metrics.totalRenters, icon: Users, tone: 'text-violet-600 dark:text-violet-300', bg: 'bg-violet-50 dark:bg-violet-500/10' },
{ label: copy.kpis[3], value: metrics.totalReservations, icon: ClipboardList, tone: 'text-orange-600 dark:text-orange-300', bg: 'bg-orange-50 dark:bg-orange-500/10' },
]
: []
if (kpis.length > 0) kpis[0].label = copy.kpis[0]
const activeRate = metrics?.totalCompanies
? Math.round((metrics.activeCompanies / metrics.totalCompanies) * 100)
: 0
const renterRatio = metrics?.totalCompanies
? Math.round(metrics.totalRenters / Math.max(metrics.totalCompanies, 1))
: 0
const actionCards = [
{ href: '/dashboard/companies', icon: Building2, title: copy.cards[0][0], body: copy.cards[0][1], cta: copy.cards[0][2] },
{ href: '/dashboard/renters', icon: Users, title: copy.cards[1][0], body: copy.cards[1][1], cta: copy.cards[1][2] },
{ href: '/dashboard/audit-logs', icon: ShieldCheck, title: copy.cards[2][0], body: copy.cards[2][1], cta: copy.cards[2][2] },
]
return (
<div className="shell py-8 space-y-8">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-emerald-400">{dict.admin}</p>
<h1 className="mt-1 text-3xl font-black">{copy.platformOverview}</h1>
<div className="grid gap-5 lg:grid-cols-[minmax(0,1.45fr)_minmax(320px,0.55fr)]">
<section className="relative overflow-hidden rounded-[2rem] border border-stone-200/80 bg-white/85 p-6 shadow-[0_30px_90px_rgba(15,23,42,0.10)] transition-colors dark:border-blue-900/60 dark:bg-[#081427]/86 dark:shadow-[0_34px_100px_rgba(0,0,0,0.34)] sm:p-8">
<div className="absolute inset-x-0 top-0 h-1 bg-[linear-gradient(90deg,#2563eb,#f97316,#10b981)]" />
<div className="flex flex-col gap-5 md:flex-row md:items-start md:justify-between">
<div className="max-w-2xl">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-orange-700 dark:text-orange-300">{copy.eyebrow}</p>
<h1 className="mt-3 text-4xl font-black tracking-normal text-blue-950 dark:text-white sm:text-5xl">{copy.platformOverview}</h1>
<p className="mt-4 max-w-2xl text-sm leading-6 text-stone-600 dark:text-slate-300">{copy.subtitle}</p>
</div>
<div className="flex w-full max-w-[15rem] items-center gap-3 rounded-2xl border border-stone-200/80 bg-stone-50/90 p-3 dark:border-blue-900/60 dark:bg-[#0d1b38]/85">
<div className="grid h-10 w-10 place-items-center rounded-xl bg-blue-950 text-white dark:bg-orange-500">
<WalletCards className="h-5 w-5" aria-hidden="true" />
</div>
<div>
<p className="text-xs text-stone-500 dark:text-slate-400">{copy.mrr}</p>
<p className="text-lg font-black text-blue-950 dark:text-white">{metrics?.mrr != null ? currencyFormatter.format(metrics.mrr) : '—'}</p>
</div>
</div>
</div>
<div className="mt-8 grid gap-3 sm:grid-cols-3">
{copy.health.map((item) => (
<div key={item} className="rounded-2xl border border-stone-200/70 bg-white/70 px-4 py-3 text-sm font-semibold text-stone-700 dark:border-blue-900/50 dark:bg-[#0d1b38]/70 dark:text-slate-200">
{item}
</div>
))}
</div>
</section>
<section className="panel p-6">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-emerald-400">{copy.liveSignal}</p>
<div className="mt-5 space-y-5">
<div>
<div className="flex items-center justify-between text-sm">
<span className="font-semibold text-zinc-200">{copy.readiness}</span>
<span className="font-black text-orange-600 dark:text-orange-300">{activeRate}%</span>
</div>
<div className="mt-2 h-2 overflow-hidden rounded-full bg-stone-200 dark:bg-blue-950">
<div className="h-full rounded-full bg-[linear-gradient(90deg,#2563eb,#f97316)]" style={{ width: `${Math.min(activeRate, 100)}%` }} />
</div>
<p className="mt-3 text-sm leading-6 text-zinc-500">{copy.readinessBody}</p>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="rounded-2xl border border-stone-200/80 bg-stone-50/80 p-4 dark:border-blue-900/50 dark:bg-[#07101e]/70">
<p className="text-xs text-zinc-500">{copy.kpis[1]}</p>
<p className="mt-1 text-2xl font-black text-zinc-200">{metrics ? numberFormatter.format(metrics.activeCompanies) : '—'}</p>
</div>
<div className="rounded-2xl border border-stone-200/80 bg-stone-50/80 p-4 dark:border-blue-900/50 dark:bg-[#07101e]/70">
<p className="text-xs text-zinc-500">{copy.kpis[2]}</p>
<p className="mt-1 text-2xl font-black text-zinc-200">{metrics ? numberFormatter.format(renterRatio) : '—'}</p>
</div>
</div>
</div>
</section>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
@@ -97,27 +197,49 @@ export default function AdminDashboardPage() {
<div className="mt-3 h-8 w-16 rounded bg-zinc-800" />
</div>
))
: kpis.map((kpi) => (
<div key={kpi.label} className="panel p-6">
<p className="text-xs text-zinc-500">{kpi.label}</p>
<p className="mt-1 text-3xl font-black">{kpi.value?.toLocaleString() ?? '—'}</p>
: kpis.length > 0 ? kpis.map((kpi) => {
const Icon = kpi.icon
return (
<div key={kpi.label} className="panel p-5">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-medium text-zinc-500">{kpi.label}</p>
<p className="mt-2 text-3xl font-black text-zinc-200">{numberFormatter.format(kpi.value ?? 0)}</p>
</div>
<div className={`grid h-10 w-10 place-items-center rounded-2xl ${kpi.bg}`}>
<Icon className={`h-5 w-5 ${kpi.tone}`} aria-hidden="true" />
</div>
</div>
</div>
)
}) : (
<div className="panel p-6 md:col-span-2 lg:col-span-4">
<p className="text-sm text-zinc-500">{copy.noMetrics}</p>
</div>
))}
)}
</div>
<div className="grid gap-6 md:grid-cols-3">
{[
['/dashboard/companies', ...copy.cards[0]],
['/dashboard/renters', ...copy.cards[1]],
['/dashboard/audit-logs', ...copy.cards[2]],
].map(([href, title, body, cta]) => (
<Link key={href} href={href} className="panel p-6 hover:border-zinc-700 transition-colors block">
<p className="text-sm font-semibold text-zinc-200">{title}</p>
<p className="mt-2 text-sm text-zinc-500">{body}</p>
<p className="mt-4 text-xs text-emerald-400 font-medium">{cta}</p>
</Link>
))}
</div>
<section>
<div className="mb-4 flex items-center justify-between">
<p className="text-sm font-bold text-blue-950 dark:text-white">{copy.quickActions}</p>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-zinc-500">{copy.brand}</p>
</div>
<div className="grid gap-5 md:grid-cols-3">
{actionCards.map(({ href, icon: Icon, title, body, cta }) => (
<Link key={href} href={href} className="panel group block p-6 hover:border-orange-300 dark:hover:border-orange-500/70">
<div className="flex items-start justify-between gap-4">
<div className="grid h-11 w-11 place-items-center rounded-2xl bg-blue-950 text-white dark:bg-orange-500">
<Icon className="h-5 w-5" aria-hidden="true" />
</div>
<ArrowRight className="h-4 w-4 text-zinc-500 transition-transform group-hover:translate-x-1 group-hover:text-orange-500" aria-hidden="true" />
</div>
<p className="mt-5 text-base font-bold text-zinc-200">{title}</p>
<p className="mt-2 min-h-[3rem] text-sm leading-6 text-zinc-500">{body}</p>
<p className="mt-5 text-xs font-bold uppercase tracking-[0.16em] text-emerald-400">{cta}</p>
</Link>
))}
</div>
</section>
</div>
)
}
+2 -2
View File
@@ -3,8 +3,8 @@ import { AdminI18nProvider } from '@/components/I18nProvider'
import './globals.css'
export const metadata: Metadata = {
title: 'RentalDriveGo Admin',
description: 'Platform administration for RentalDriveGo.',
title: 'FleetOS Admin',
description: 'FleetOS platform administration.',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {