From 17dc53fcfe15aaec0bf9dc1498551f8e1925f7d3 Mon Sep 17 00:00:00 2001 From: m7amedez5511 Date: Mon, 27 Jul 2026 17:54:43 +0300 Subject: [PATCH] build dashboard home page and dashboard layer --- app/dashboard/page.tsx | 253 ++++++------------ src/Components/Dashboard/AlertsSection.tsx | 90 +++++++ src/Components/Dashboard/KpiSection.tsx | 121 +++++++++ .../Dashboard/QuickAccessFooter.tsx | 42 +++ .../Dashboard/RecentActivityPanel.tsx | 71 +++++ src/Components/Dashboard/TrendChartsPanel.tsx | 81 ++++++ src/Components/Dashboard/index.ts | 5 + src/hooks/useDashboardOverview.ts | 40 +++ src/services/dashboard.service.ts | 71 ++++- src/types/dashboard.ts | 119 +++++++- 10 files changed, 726 insertions(+), 167 deletions(-) create mode 100644 src/Components/Dashboard/AlertsSection.tsx create mode 100644 src/Components/Dashboard/KpiSection.tsx create mode 100644 src/Components/Dashboard/QuickAccessFooter.tsx create mode 100644 src/Components/Dashboard/RecentActivityPanel.tsx create mode 100644 src/Components/Dashboard/TrendChartsPanel.tsx create mode 100644 src/Components/Dashboard/index.ts create mode 100644 src/hooks/useDashboardOverview.ts diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index c67aff1..9834dfe 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,15 +1,21 @@ "use client"; -import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect, useMemo } from "react"; -import { useDashboardSummary } from "@/src/hooks/useDashboardSummary"; +import { useEffect } from "react"; import { clearAuth, getStoredUser } from "@/src/lib/auth"; import { Spinner, Alert } from "@/src/Components/UI"; +import { useDashboardOverview } from "@/src/hooks/useDashboardOverview"; +import { + KpiSection, + AlertsSection, + RecentActivityPanel, + TrendChartsPanel, + QuickAccessFooter, +} from "@/src/Components/Dashboard"; export default function DashboardPage() { const router = useRouter(); - const { data, error, loading } = useDashboardSummary(); + const { data, error, loading } = useDashboardOverview(); useEffect(() => { const storedUser = getStoredUser(); @@ -20,20 +26,8 @@ export default function DashboardPage() { } }, [router]); - const stats = data?.stats; - const alerts = data?.alerts; const activeTrips = data?.activeTrips || []; - const summaryCards = useMemo( - () => [ - { label: "العملاء", value: stats?.clients ?? 0, accent: "#06B6D4" }, - { label: "الطلبات", value: stats?.orders ?? 0, accent: "#A78BFA" }, - { label: "الرحلات", value: stats?.trips ?? 0, accent: "#34D399" }, - { label: "المركبات و السائقين ", value: (stats?.cars ?? 0) + (stats?.drivers ?? 0), accent: "#FBBF24" }, - ], - [stats], - ); - return (
@@ -57,15 +51,14 @@ export default function DashboardPage() { ذكاء الأسطول في لمحة سريعة

- عرض واضح لطلبات الأسطول وتنبيهات السلامة وتقدم الرحلات. + عرض واضح لكل وحدة من وحدات المنصة، مع تنبيهات فورية واتجاهات الأداء.

- {/* ── Loading ── */} - {loading && ( + {loading && !data && (
جارٍ تحميل البيانات… @@ -74,163 +67,93 @@ export default function DashboardPage() { {/* ── Error ── */} {error && ( - + )} - {!loading && !error && ( - <> - {/* ── Summary cards ── */} -
- {summaryCards.map((item) => ( -
-
-

- {item.label} -

-

- {item.value} -

-
- ))} -
+ {/* ── KPI Section: total / active / pending per entity (9 cards) ── */} + - {/* ── Active trips + alerts ── - Column order in the template below stays [trips, alerts] — - same reading order as before. In RTL this now renders - trips on the right (read first) and alerts on the left, - which is the correct mirror; no class change needed (see - grid + RTL note in the project report). */} -
-
-
+ {/* ── Alerts Section: urgent cross-entity issues ── */} + + + {/* ── Insights Row: recent activity log + trend charts ── */} +
+ + +
+ + {/* ── Quick access to every entity page (minimizes navigation clicks) ── */} + + + {/* ── Active trips (kept from the previous layout — live operational detail) ── */} +
+
+
+

الرحلات النشطة

+

تقدم الرحلات

+
+ + مباشر + +
+
+ {activeTrips.length ? activeTrips.map((trip) => ( +
+
-

الرحلات النشطة

-

تقدم الرحلات

+

+ {trip.tripNumber} +

+

{trip.title}

- - مباشر + + {trip.progress}%
-
- {activeTrips.length ? activeTrips.map((trip) => ( -
-
-
-

- {trip.tripNumber} -

-

{trip.title}

-
- - {trip.progress}% - -
-
- {/* - Progress fill: width % growing from the bar's - start edge is correct in both directions since - `width` itself is non-directional — the bar's - own inline-start (right, in RTL) is where the - fill begins, matching how the rest of the RTL - layout fills from the right. No change needed. - */} -
-
-
- )) : ( -

لا توجد رحلات نشطة حالياً.

- )} +
+
-
+
+ )) : ( +

لا توجد رحلات نشطة حالياً.

+ )} +
+ -
-

تنبيهات الالتزام

-

التجديدات القادمة

-
- {(alerts?.expiringCars || []).slice(0, 3).map((item, i) => ( -
- {String((item as Record).message || "Vehicle expiry alert")} -
- ))} - {(alerts?.expiringDrivers || []).slice(0, 3).map((item, i) => ( -
- {String((item as Record).message || "Driver expiry alert")} -
- ))} - {(alerts?.upcomingMaint || []).slice(0, 3).map((item, i) => ( -
- {String((item as Record).message || "Maintenance alert")} -
- ))} - {!(alerts?.expiringCars?.length || alerts?.expiringDrivers?.length || alerts?.upcomingMaint?.length) && ( -

لا توجد تنبيهات حالياً.

- )} -
-
+ {/* ── Security + endpoints ── */} +
+
+

الأمان

+

لمحة الحساب

+
+
+ آخر تسجيل دخول: {data?.accountSecurity?.lastLogin || "—"} +
+
+ بيانات الجهاز: {data?.accountSecurity?.requestMeta ? JSON.stringify(data.accountSecurity.requestMeta) : "—"} +
+
- {/* ── Security + endpoints ── */} -
-
-

الأمان

-

لمحة الحساب

-
-
- آخر تسجيل دخول: {data?.accountSecurity?.lastLogin || "—"} -
-
- {/* - JSON.stringify output is structurally LTR (braces, - colons, commas read left-to-right regardless of - locale). Wrapping it in .ltr-embed prevents the - surrounding RTL paragraph from reordering the - punctuation — without this, nested objects can - render with their braces visually swapped. - */} - بيانات الجهاز: {data?.accountSecurity?.requestMeta ? JSON.stringify(data.accountSecurity.requestMeta) : "—"} -
-
-
- -
-

خريطة الخلفية

-

النقاط النهائية

-
    - {[ - "/v1/dashboard/summary — نظرة عامة تشغيلية", - "/v1/client — إدارة العملاء", - "/v1/orders — دورة شحن الطلبات", - "/v1/trip — تنظيم الرحلات", - ].map((ep) => ( -
  • - {/* - Each entry mixes an LTR API path with an Arabic - description. Splitting on the em-dash and - isolating only the path keeps the route string - from reading backwards while letting the Arabic - half flow naturally in the paragraph's own - direction. - */} - {ep.split(" — ")[0]} - {" — "} - {ep.split(" — ")[1]} -
  • - ))} -
-
-
- - )} +
+

خريطة الخلفية

+

النقاط النهائية

+
    + {[ + "/v1/dashboard/overview — نظرة عامة تشغيلية", + "/v1/client — إدارة العملاء", + "/v1/orders — دورة شحن الطلبات", + "/v1/trip — تنظيم الرحلات", + ].map((ep) => ( +
  • + {ep.split(" — ")[0]} + {" — "} + {ep.split(" — ")[1]} +
  • + ))} +
+
+
); } \ No newline at end of file diff --git a/src/Components/Dashboard/AlertsSection.tsx b/src/Components/Dashboard/AlertsSection.tsx new file mode 100644 index 0000000..a843ddd --- /dev/null +++ b/src/Components/Dashboard/AlertsSection.tsx @@ -0,0 +1,90 @@ +"use client"; + +import Link from "next/link"; +import { EmptyState, Spinner } from "@/src/Components/UI"; +import { ENTITY_KPI_CONFIG } from "@/src/types/dashboard"; +import type { DashboardAlert } from "@/src/types/dashboard"; + +function fmtRelative(iso: string): string { + const diffMs = Date.now() - new Date(iso).getTime(); + const mins = Math.floor(diffMs / 60000); + if (mins < 1) return "الآن"; + if (mins < 60) return `منذ ${mins} د`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `منذ ${hours} س`; + return `منذ ${Math.floor(hours / 24)} يوم`; +} + +function AlertListItem({ alert }: { alert: DashboardAlert }) { + const entityCfg = ENTITY_KPI_CONFIG.find((e) => e.key === alert.entity); + const isCritical = alert.severity === "critical"; + + return ( + + + + {alert.message} + + {entityCfg && ( + + {entityCfg.label} + + )} + + {fmtRelative(alert.createdAt)} + + + ); +} + +interface AlertsSectionProps { + alerts: DashboardAlert[]; + loading: boolean; +} + +export function AlertsSection({ alerts, loading }: AlertsSectionProps) { + return ( +
+
+
+

+ تنبيهات عاجلة +

+

+ يحتاج انتباهك +

+
+ {loading && } +
+ +
+ {!loading && alerts.length === 0 && } + {alerts.map((alert) => ( + + ))} +
+
+ ); +} \ No newline at end of file diff --git a/src/Components/Dashboard/KpiSection.tsx b/src/Components/Dashboard/KpiSection.tsx new file mode 100644 index 0000000..0de6a26 --- /dev/null +++ b/src/Components/Dashboard/KpiSection.tsx @@ -0,0 +1,121 @@ +"use client"; + +import Link from "next/link"; +import { Spinner } from "@/src/Components/UI"; +import type { EntityKpi } from "@/src/types/dashboard"; +import { ENTITY_KPI_CONFIG, EMPTY_ENTITY_KPI } from "@/src/types/dashboard"; + +function KpiAnomalyBadge({ anomaly }: { anomaly: EntityKpi["anomaly"] }) { + if (!anomaly) return null; + const isCritical = anomaly.severity === "critical"; + return ( +
+ + + {anomaly.message} + +
+ ); +} + +function KpiCardCounts({ total, active, pending }: { total: number; active: number; pending: number }) { + return ( +
+

+ {total.toLocaleString("ar-SA")} +

+
+
+

نشط

+

+ {active.toLocaleString("ar-SA")} +

+
+
+

معلَّق

+

+ {pending.toLocaleString("ar-SA")} +

+
+
+
+ ); +} + +function KpiCard({ entity }: { entity: EntityKpi }) { + return ( + +
+
+
+