From 1e4d9c5b597d40b5a124903ffbfac4674dd5464d Mon Sep 17 00:00:00 2001 From: m7amedez5511 Date: Sun, 30 Aug 2026 16:18:54 +0300 Subject: [PATCH] update dashboard home --- src/Components/Dashboard/KpiSection.tsx | 28 ++++++------- src/lib/locationHierarchy.ts | 2 - src/services/audit.service.ts | 35 ++++++++++++++++ src/services/dashboard.service.ts | 55 +++++++++++++++++++++---- src/types/dashboard.ts | 12 +++--- 5 files changed, 102 insertions(+), 30 deletions(-) create mode 100644 src/services/audit.service.ts diff --git a/src/Components/Dashboard/KpiSection.tsx b/src/Components/Dashboard/KpiSection.tsx index 0de6a26..0b4db0e 100644 --- a/src/Components/Dashboard/KpiSection.tsx +++ b/src/Components/Dashboard/KpiSection.tsx @@ -29,26 +29,18 @@ function KpiAnomalyBadge({ anomaly }: { anomaly: EntityKpi["anomaly"] }) { ); } -function KpiCardCounts({ total, active, pending }: { total: number; active: number; pending: number }) { +// Step 8: KpiCardCounts no longer takes/renders `active`/`pending` — it now + +// only receives and displays `total`. The active/pending mini-boxes (and +// their inline styling) have been removed entirely; the total figure keeps +// its original styling/position exactly as before, so the card's overall +// layout is unchanged. +function KpiCardCounts({ total }: { total: number }) { return (

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

-
-
-

نشط

-

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

-
-
-

معلَّق

-

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

-
-
); } @@ -71,6 +63,7 @@ function KpiCard({ entity }: { entity: EntityKpi }) { >
+ - + {/* Step 9: only `total` is passed now — `active`/`pending` props removed + from this call site along with the corresponding fields on EntityKpi. */} + ); @@ -101,6 +96,7 @@ interface KpiSectionProps { export function KpiSection({ entities, loading }: KpiSectionProps) { // Before the overview resolves, render the static config as zeroed cards // so the grid never collapses to nothing. + const list = entities.length > 0 ? entities : ENTITY_KPI_CONFIG.map((cfg) => ({ ...cfg, ...EMPTY_ENTITY_KPI })); return ( diff --git a/src/lib/locationHierarchy.ts b/src/lib/locationHierarchy.ts index c635eb8..01b7588 100644 --- a/src/lib/locationHierarchy.ts +++ b/src/lib/locationHierarchy.ts @@ -1,5 +1,3 @@ -// src/lib/locationHierarchy.ts -// // Step 1: Pure lookup helpers over SAUDI_LOCATION_HIERARCHY. No side // effects, no fetch — the dataset is static and imported directly, so // these functions are safe to call from both client components and the diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts new file mode 100644 index 0000000..d12a894 --- /dev/null +++ b/src/services/audit.service.ts @@ -0,0 +1,35 @@ +import { get } from "./api"; +import type { AuditLog } from "@/src/types/audit"; + +// Step 1: New service, following the exact same shape/pattern as the other +// list services in this file's siblings (userService.getAll, branchService. +// getAll, roleService.getAll) — GET with page/limit, unwrap { data: { data, +// meta|pagination } } into a clean { items, total } result. No audit service +// previously existed even though src/types/audit.ts and AuditTable already +// consume AuditLog, so this fills that gap using the established contract. +export interface AuditListResponse { + success: boolean; + message: string; + responseAt: string; + data: { + data: AuditLog[]; + meta?: { total: number; page: number; limit: number; totalPages: number }; + pagination?: { total: number; page: number; pages: number }; + }; +} + +function buildAuditQuery(page: number, limit: number): string { + return `?page=${page}&limit=${limit}`; +} + +export const auditService = { + // The real backend endpoint is /api/v1/audit and it responds with + // { data: { data: [...], meta: { total, page, limit, totalPages } } }. + getAll: async (page = 1, limit = 1): Promise<{ items: AuditLog[]; total: number }> => { + const res = await get(`audit${buildAuditQuery(page, limit)}`); + return { + items: res.data?.data ?? [], + total: res.data?.meta?.total ?? res.data?.pagination?.total ?? 0, + }; + }, +}; \ No newline at end of file diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts index d45f05b..c1275ca 100644 --- a/src/services/dashboard.service.ts +++ b/src/services/dashboard.service.ts @@ -6,17 +6,35 @@ import type { DashboardAlert, } from "@/src/types/dashboard"; import { ENTITY_KPI_CONFIG, EMPTY_ENTITY_KPI } from "@/src/types/dashboard"; +// Step 3: Reuse the exact same services already used by UserTable, +// BranchTable, and RoleTable (via useUsers/useBranches/useRoles) instead of +// writing new fetch logic — this keeps the dashboard's data-fetching +// pattern identical to the rest of the app, per the task instructions. +import { userService } from "./user.service"; +import { branchService } from "./branch.service"; +import { roleService } from "./role.service"; +import { auditService } from "./audit.service"; -// Maps the stats we DO get from /dashboard/summary onto the entity cards. -// Entities with no matching stat (users, branches, roles, audit) stay at 0 -// until the real /dashboard/overview endpoint exists. -function buildEntities(stats: DashboardSummaryResponse["data"]["stats"]): EntityKpi[] { +// Step 4: buildEntities now takes a second argument with the totals that +// don't come from /dashboard/summary (users, branches, roles, audit) and +// merges them into the same totals map the clients/orders/trips/cars/ +// drivers stats already flow through — no change to the entity list shape +// or ordering (ENTITY_KPI_CONFIG is untouched), so layout stays identical. +function buildEntities( + stats: DashboardSummaryResponse["data"]["stats"], + extraTotals: { users: number; branches: number; roles: number; audit: number }, +): EntityKpi[] { const totals: Partial> = { clients: stats.clients, orders: stats.orders, trips: stats.trips, cars: stats.cars, drivers: stats.drivers, + + users: extraTotals.users, + branches: extraTotals.branches, + roles: extraTotals.roles, + audit: extraTotals.audit, }; return ENTITY_KPI_CONFIG.map((cfg) => ({ @@ -45,6 +63,7 @@ function buildAlerts(alerts: DashboardSummaryResponse["data"]["alerts"]): Dashbo createdAt: now, })); + return [ ...mapGroup(alerts.expiringCars, "cars", "warning", "expiring-car"), ...mapGroup(alerts.expiringDrivers, "drivers", "warning", "expiring-driver"), @@ -57,15 +76,37 @@ export const dashboardService = { get("dashboard/summary", token), // NOTE: /dashboard/overview isn't exposed by the backend yet, so this - // composes the DashboardOverview shape client-side from /dashboard/summary, - // same "additive, not destructive" pattern used elsewhere (see + // composes the DashboardOverview shape client-side from /dashboard/summary + // plus the users/branches/roles/audit totals fetched below, same + // "additive, not destructive" pattern used elsewhere (see // useArchivedCars/useArchivedRoles) while the real route isn't live. getOverview: async (token: string | null): Promise => { const res = await get("dashboard/summary", token ?? ""); const { stats, alerts, activeTrips, accountSecurity } = res.data; + // Step 5: Fetch the four missing totals in parallel (mirrors the + // Promise.all-free-but-concurrent pattern already used in + // OrderFormModal, which loads clients + trips together for the same + // modal mount). page=1 + empty search is exactly what UserTable / + // BranchTable / RoleTable already send on first load — we just read + // back `.total` and discard the `.items` page since the KPI card only + // needs the count. Each call is defensively caught so one failing + // endpoint doesn't blank out the other three cards. + const [usersResult, branchesResult, rolesResult, auditResult] = await Promise.all([ + userService.getAll(1, "", token).catch(() => ({ items: [], total: 0, pages: 1 })), + branchService.getAll(1, "", token).catch(() => ({ items: [], total: 0, pages: 1 })), + roleService.getAll(1, "", token).catch(() => ({ items: [], total: 0, pages: 1 })), + + auditService.getAll(1, 1).catch(() => ({ items: [], total: 0 })), + ]); + return { - entities: buildEntities(stats), + entities: buildEntities(stats, { + users: usersResult.total, + branches: branchesResult.total, + roles: rolesResult.total, + audit: auditResult.total, + }), alerts: buildAlerts(alerts), trends: [], // no trend endpoint yet recentActivity: [], // no audit-log endpoint composed yet diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts index 4706e85..181d908 100644 --- a/src/types/dashboard.ts +++ b/src/types/dashboard.ts @@ -90,6 +90,10 @@ export type DashboardEntityKey = | "users" | "drivers" | "cars" | "trips" | "orders" | "clients" | "branches" | "roles" | "audit"; +// Step 6: Removed `active` and `pending` from EntityKpi — the stat cards +// (KpiSection.tsx) no longer break totals down into active/pending, they +// only display the total count, so the type no longer carries fields that +// are never rendered. export interface EntityKpi { key: DashboardEntityKey; label: string; @@ -99,18 +103,16 @@ export interface EntityKpi { accent: string; href: string; total: number; - active: number; - pending: number; anomaly: { severity: "warning" | "critical"; message: string; } | null; } -export const EMPTY_ENTITY_KPI: Pick = { +// Step 7: EMPTY_ENTITY_KPI narrowed to match the trimmed EntityKpi shape — +// only `total` and `anomaly` are defaultable now. +export const EMPTY_ENTITY_KPI: Pick = { total: 0, - active: 0, - pending: 0, anomaly: null, };