update dashboard home
This commit is contained in:
@@ -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 (
|
||||
<div style={{ marginTop: "0.875rem" }}>
|
||||
<p style={{ fontSize: "2rem", fontWeight: 700, color: "var(--color-text-dark-primary)", fontFamily: "var(--font-mono)", margin: 0, textAlign: "start" }}>
|
||||
{total.toLocaleString("ar-SA")}
|
||||
</p>
|
||||
<div style={{ marginTop: "0.625rem", display: "flex", gap: "0.75rem" }}>
|
||||
<div style={{ flex: 1, borderRadius: "var(--radius-md)", background: "rgba(52,211,153,0.08)", border: "1px solid rgba(52,211,153,0.2)", padding: "0.375rem 0.625rem" }}>
|
||||
<p style={{ fontSize: 10, color: "#6EE7B7", margin: 0 }}>نشط</p>
|
||||
<p style={{ fontSize: 13, fontWeight: 700, color: "#A7F3D0", margin: 0, fontFamily: "var(--font-mono)" }}>
|
||||
{active.toLocaleString("ar-SA")}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ flex: 1, borderRadius: "var(--radius-md)", background: "rgba(251,191,36,0.08)", border: "1px solid rgba(251,191,36,0.2)", padding: "0.375rem 0.625rem" }}>
|
||||
<p style={{ fontSize: 10, color: "#FCD34D", margin: 0 }}>معلَّق</p>
|
||||
<p style={{ fontSize: 13, fontWeight: 700, color: "#FDE68A", margin: 0, fontFamily: "var(--font-mono)" }}>
|
||||
{pending.toLocaleString("ar-SA")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -71,6 +63,7 @@ function KpiCard({ entity }: { entity: EntityKpi }) {
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: 40, height: 40, borderRadius: "var(--radius-lg)",
|
||||
@@ -87,7 +80,9 @@ function KpiCard({ entity }: { entity: EntityKpi }) {
|
||||
<i className="ti ti-chevron-left" style={{ fontSize: 14, color: "var(--color-text-dark-muted)" }} aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<KpiCardCounts total={entity.total} active={entity.active} pending={entity.pending} />
|
||||
{/* Step 9: only `total` is passed now — `active`/`pending` props removed
|
||||
from this call site along with the corresponding fields on EntityKpi. */}
|
||||
<KpiCardCounts total={entity.total} />
|
||||
<KpiAnomalyBadge anomaly={entity.anomaly} />
|
||||
</Link>
|
||||
);
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
35
src/services/audit.service.ts
Normal file
35
src/services/audit.service.ts
Normal file
@@ -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<AuditListResponse>(`audit${buildAuditQuery(page, limit)}`);
|
||||
return {
|
||||
items: res.data?.data ?? [],
|
||||
total: res.data?.meta?.total ?? res.data?.pagination?.total ?? 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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<Record<EntityKpi["key"], number>> = {
|
||||
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<DashboardSummaryResponse>("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<DashboardOverview> => {
|
||||
const res = await get<DashboardSummaryResponse>("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
|
||||
|
||||
@@ -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<EntityKpi, "total" | "active" | "pending" | "anomaly"> = {
|
||||
// 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<EntityKpi, "total" | "anomaly"> = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
pending: 0,
|
||||
anomaly: null,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user