154 lines
9.0 KiB
TypeScript
154 lines
9.0 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import { useRouter } from "next/navigation";
|
||
import { useEffect, useMemo, useState } from "react";
|
||
import { getDashboardSummary, type DashboardSummaryResponse } from "../lib/api";
|
||
import { getStoredToken } from "../lib/auth";
|
||
|
||
export default function DashboardPage() {
|
||
const router = useRouter();
|
||
const [data, setData] = useState<DashboardSummaryResponse | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
if (!getStoredToken()) {
|
||
router.replace("/login");
|
||
return;
|
||
}
|
||
|
||
let mounted = true;
|
||
|
||
async function load() {
|
||
try {
|
||
const summary = await getDashboardSummary();
|
||
if (mounted) setData(summary);
|
||
} catch (err) {
|
||
if (mounted) setError(err instanceof Error ? err.message : "Unable to load dashboard data.");
|
||
} finally {
|
||
if (mounted) setLoading(false);
|
||
}
|
||
}
|
||
|
||
load();
|
||
return () => {
|
||
mounted = false;
|
||
};
|
||
}, [router]);
|
||
|
||
const stats = data?.data?.stats;
|
||
const alerts = data?.data?.alerts;
|
||
const activeTrips = data?.data?.activeTrips || [];
|
||
|
||
const summaryCards = useMemo(
|
||
() => [
|
||
{ label: "العملاء", value: stats?.clients ?? 0, tone: "from-cyan-500 to-sky-600" },
|
||
{ label: "الطلبات", value: stats?.orders ?? 0, tone: "from-violet-500 to-fuchsia-600" },
|
||
{ label: "الرحلات", value: stats?.trips ?? 0, tone: "from-emerald-500 to-green-600" },
|
||
{ label: "المركبات", value: (stats?.cars ?? 0) + (stats?.drivers ?? 0), tone: "from-amber-400 to-orange-500" },
|
||
],
|
||
[stats],
|
||
);
|
||
|
||
return (
|
||
<main className="min-h-screen bg-[radial-gradient(circle_at_top,#0f172a_0%,#111827_45%,#020617_100%)] text-slate-100">
|
||
<section className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-6 py-10 lg:px-8">
|
||
<header className="rounded-3xl border border-white/10 bg-white/6 p-8 shadow-2xl shadow-slate-950/30 backdrop-blur-xl">
|
||
<p className="text-sm uppercase tracking-[0.35em] text-cyan-200">لوحة العمليات</p>
|
||
<div className="mt-4 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-semibold text-white lg:text-4xl">ذكاء الأسطول في لمحة سريعة</h1>
|
||
<p className="mt-3 max-w-3xl text-slate-300">هذه الشاشة متوافقة مع نقطة ملخص لوحة الإدارة في الخلفية وتوفر عرضًا واضحًا لطلب الأسطول، وتنبيهات السلامة، وتقدم الرحلات.</p>
|
||
</div>
|
||
<Link href="/" className="inline-flex items-center rounded-full border border-cyan-400/30 bg-cyan-400/10 px-4 py-2 text-sm text-cyan-100 transition hover:bg-cyan-400/20">العودة إلى بوابة العميل</Link>
|
||
</div>
|
||
</header>
|
||
|
||
{loading && <p className="text-slate-300">جارٍ تحميل مقاييس الخلفية…</p>}
|
||
{error && <p className="rounded-2xl border border-rose-400/30 bg-rose-500/10 p-4 text-rose-100">{error}</p>}
|
||
|
||
{!loading && !error && (
|
||
<>
|
||
<section className="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||
{summaryCards.map((item) => (
|
||
<article key={item.label} className="rounded-3xl border border-white/10 bg-slate-900/80 p-5 shadow-xl shadow-slate-950/20">
|
||
<div className={`h-2 rounded-full bg-linear-to-r ${item.tone}`} />
|
||
<p className="mt-4 text-sm uppercase tracking-[0.25em] text-slate-400">{item.label}</p>
|
||
<p className="mt-2 text-4xl font-semibold text-white">{item.value}</p>
|
||
</article>
|
||
))}
|
||
</section>
|
||
|
||
<section className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
||
<article className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm uppercase tracking-[0.25em] text-cyan-200">الرحلات النشطة</p>
|
||
<h2 className="mt-2 text-xl font-semibold text-white">تقدم الرحلات</h2>
|
||
</div>
|
||
<span className="rounded-full bg-emerald-400/10 px-3 py-1 text-xs text-emerald-200">مباشر من /v1/dashboard/summary</span>
|
||
</div>
|
||
<div className="mt-6 space-y-4">
|
||
{activeTrips.length ? activeTrips.map((trip) => (
|
||
<div key={trip.id} className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">
|
||
<div className="flex items-center justify-between gap-3">
|
||
<div>
|
||
<p className="text-sm font-semibold text-white">{trip.tripNumber}</p>
|
||
<p className="text-sm text-slate-300">{trip.title}</p>
|
||
</div>
|
||
<span className="rounded-full bg-cyan-400/10 px-3 py-1 text-xs text-cyan-100">{trip.progress}%</span>
|
||
</div>
|
||
<div className="mt-3 h-2 rounded-full bg-slate-700">
|
||
<div className="h-2 rounded-full bg-linear-to-r from-cyan-400 to-emerald-400" style={{ width: `${trip.progress}%` }} />
|
||
</div>
|
||
</div>
|
||
)) : <p className="text-sm text-slate-300">لا توجد رحلات في-progress حالياً.</p>}
|
||
</div>
|
||
</article>
|
||
|
||
<article className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20">
|
||
<p className="text-sm uppercase tracking-[0.25em] text-amber-200">تنبيهات الالتزام</p>
|
||
<h2 className="mt-2 text-xl font-semibold text-white">التجديدات القادمة</h2>
|
||
<div className="mt-6 space-y-4 text-sm text-slate-200">
|
||
{(alerts?.expiringCars || []).slice(0, 3).map((item, index) => (
|
||
<div key={`car-${index}`} className="rounded-2xl border border-amber-400/20 bg-amber-400/8 p-4">{String((item as Record<string, unknown>).message || "Vehicle expiry alert")}</div>
|
||
))}
|
||
{(alerts?.expiringDrivers || []).slice(0, 3).map((item, index) => (
|
||
<div key={`driver-${index}`} className="rounded-2xl border border-rose-400/20 bg-rose-500/8 p-4">{String((item as Record<string, unknown>).message || "Driver expiry alert")}</div>
|
||
))}
|
||
{(alerts?.upcomingMaint || []).slice(0, 3).map((item, index) => (
|
||
<div key={`maint-${index}`} className="rounded-2xl border border-cyan-400/20 bg-cyan-400/8 p-4">{String((item as Record<string, unknown>).message || "Maintenance alert")}</div>
|
||
))}
|
||
</div>
|
||
</article>
|
||
</section>
|
||
|
||
<section className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
|
||
<article className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20">
|
||
<p className="text-sm uppercase tracking-[0.25em] text-violet-200">الأمان</p>
|
||
<h2 className="mt-2 text-xl font-semibold text-white">لمحة الحساب</h2>
|
||
<div className="mt-6 space-y-4">
|
||
<div className="rounded-2xl border border-white/10 bg-slate-800/80 p-4 text-sm text-slate-200">آخر تسجيل دخول: {data?.data?.accountSecurity?.lastLogin || "لم يتم العثور على حدث تسجيل دخول"}</div>
|
||
<div className="rounded-2xl border border-white/10 bg-slate-800/80 p-4 text-sm text-slate-200">بيانات الجهاز: {data?.data?.accountSecurity?.requestMeta ? JSON.stringify(data.data.accountSecurity.requestMeta) : "غير متاح"}</div>
|
||
</div>
|
||
</article>
|
||
|
||
<article className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20">
|
||
<p className="text-sm uppercase tracking-[0.25em] text-emerald-200">خريطة الخلفية</p>
|
||
<h2 className="mt-2 text-xl font-semibold text-white">النقاط النهائية المستندة إلى Swagger</h2>
|
||
<ul className="mt-6 grid gap-3 text-sm text-slate-200">
|
||
<li className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">/v1/dashboard/summary — نظرة عامة تشغيلية</li>
|
||
<li className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">/v1/client — إدارة العملاء</li>
|
||
<li className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">/v1/orders — دورة شحن الطلبات</li>
|
||
<li className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">/v1/trip — تنظيم الرحلات</li>
|
||
</ul>
|
||
</article>
|
||
</section>
|
||
</>
|
||
)}
|
||
</section>
|
||
</main>
|
||
);
|
||
}
|