finsh user,car,driver pages also add validator layer to app build trip page

This commit is contained in:
m7amedez5511
2026-06-18 16:51:31 +03:00
parent a23d21f222
commit c1b77f89cd
45 changed files with 4128 additions and 1690 deletions

View File

@@ -0,0 +1,624 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Spinner } from "@/Components/UI";
import { DriverFormModal } from "@/Components/Driver/DriverFormModal";
import { DriverDeleteModal } from "@/Components/Driver/DriverDeleteModal";
import { DriverReportPanel } from "@/Components/Driver_Report/driverReport";
import { driverService } from "@/services";
import { getStoredToken } from "@/lib/auth";
import type {
Driver,
CreateDriverPayload,
UpdateDriverPayload,
} from "@/types/driver";
import {
DRIVER_STATUS_MAP,
DRIVER_CARD_TYPE_MAP,
NATIONAL_ID_TYPE_MAP,
} from "@/types/driver";
// ── Helpers ───────────────────────────────────────────────────────────────────
function fmtDate(iso?: string | null): string {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("ar-SA", {
year: "numeric",
month: "long",
day: "numeric",
});
}
function isExpiringSoon(iso?: string | null): boolean {
if (!iso) return false;
return (new Date(iso).getTime() - Date.now()) <= 90 * 86_400_000;
}
// ── Sub-components ────────────────────────────────────────────────────────────
function SectionCard({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div
style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
overflow: "hidden",
boxShadow: "var(--shadow-card)",
}}
>
<div
style={{
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
}}
>
<p
style={{
fontSize: 11,
letterSpacing: "0.25em",
textTransform: "uppercase",
color: "var(--color-text-hint)",
fontWeight: 700,
margin: 0,
}}
>
{title}
</p>
</div>
<div style={{ padding: "1rem 1.5rem" }}>{children}</div>
</div>
);
}
function DetailRow({
label,
value,
mono = false,
warn = false,
}: {
label: string;
value: string;
mono?: boolean;
warn?: boolean;
}) {
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
padding: "0.55rem 0",
borderBottom: "1px solid var(--color-border)",
}}
>
<span style={{ fontSize: 12, color: "var(--color-text-muted)", fontWeight: 600 }}>
{label}
</span>
<span
style={{
fontSize: 13,
fontFamily: mono ? "var(--font-mono)" : "var(--font-sans)",
color: warn ? "#D97706" : "var(--color-text-primary)",
fontWeight: warn ? 600 : 400,
maxWidth: "60%",
textAlign: "left",
wordBreak: "break-word",
}}
>
{warn && value !== "—" ? "⚠ " : ""}
{value}
</span>
</div>
);
}
function PhotoCard({ url, label }: { url?: string | null; label: string }) {
const [imgError, setImgError] = useState(false);
return (
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: "var(--color-text-muted)" }}>
{label}
</span>
<div
style={{
width: "100%",
aspectRatio: "4/3",
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{url && !imgError ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={url}
alt={label}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
onError={() => setImgError(true)}
/>
) : (
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="var(--color-text-hint)"
strokeWidth="1.5"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
)}
</div>
{url && !imgError && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
style={{ fontSize: 11, color: "#2563EB", textDecoration: "underline", textAlign: "center" }}
>
عرض الصورة
</a>
)}
</div>
);
}
// ── Page ──────────────────────────────────────────────────────────────────────
export default function DriverDetailPage() {
const params = useParams();
const router = useRouter();
const driverId = params?.driverId as string;
const [driver, setDriver] = useState<Driver | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [avatarError, setAvatarError] = useState(false);
// ── Modal state ───────────────────────────────────────────────────────────
const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
// ── Load driver ───────────────────────────────────────────────────────────
const loadDriver = useCallback(async () => {
if (!driverId) return;
setLoading(true);
setError(null);
try {
const token = getStoredToken();
const res = await driverService.getById(driverId, token);
setDriver((res as unknown as { data: Driver }).data);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "تعذّر تحميل بيانات السائق.");
} finally {
setLoading(false);
}
}, [driverId]);
useEffect(() => {
loadDriver();
}, [loadDriver]);
// ── Edit submit ───────────────────────────────────────────────────────────
const handleEditSubmit = useCallback(
async (
payload: CreateDriverPayload | UpdateDriverPayload,
_isNew: boolean,
): Promise<boolean> => {
if (!driver) return false;
try {
const token = getStoredToken();
await driverService.update(driver.id, payload as UpdateDriverPayload, token);
await loadDriver();
return true;
} catch {
return false;
}
},
[driver, loadDriver],
);
// ── Delete confirm ────────────────────────────────────────────────────────
const handleConfirmDelete = useCallback(async () => {
if (!driver) return;
setDeleting(true);
try {
const token = getStoredToken();
await driverService.delete(driver.id, token);
router.push("/dashboard/drivers");
} catch {
setDeleting(false);
}
}, [driver, router]);
// ── Status config ─────────────────────────────────────────────────────────
const statusConfig = driver ? DRIVER_STATUS_MAP[driver.status] : null;
// ── Render: Loading ───────────────────────────────────────────────────────
if (loading) {
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 12,
padding: "6rem 0",
color: "var(--color-text-muted)",
}}
>
<Spinner size="sm" className="text-blue-600" />
<span style={{ fontSize: 14 }}>جارٍ التحميل</span>
</div>
);
}
// ── Render: Error ─────────────────────────────────────────────────────────
if (error || !driver) {
return (
<div
style={{
maxWidth: 480,
margin: "4rem auto",
borderRadius: "var(--radius-xl)",
border: "1px solid #FECACA",
background: "#FEF2F2",
padding: "1.5rem",
textAlign: "center",
}}
>
<p style={{ fontSize: 14, color: "#DC2626", fontWeight: 600 }}>
{error ?? "السائق غير موجود"}
</p>
<button
type="button"
onClick={() => router.back()}
style={{
marginTop: "1rem",
height: 38,
padding: "0 1.25rem",
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13,
fontWeight: 600,
color: "var(--color-text-secondary)",
cursor: "pointer",
fontFamily: "var(--font-sans)",
}}
>
رجوع
</button>
</div>
);
}
// ── Render: Driver detail ─────────────────────────────────────────────────
return (
<>
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* ── Page header ── */}
<header
style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
padding: "1.5rem 2rem",
boxShadow: "var(--shadow-card)",
}}
>
{/* Back link */}
<button
type="button"
onClick={() => router.back()}
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
fontSize: 12,
fontWeight: 600,
color: "var(--color-text-muted)",
background: "none",
border: "none",
cursor: "pointer",
padding: 0,
marginBottom: "1rem",
fontFamily: "var(--font-sans)",
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M19 12H5M12 5l-7 7 7 7" />
</svg>
العودة إلى قائمة السائقين
</button>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: "1rem" }}>
{/* Avatar + name */}
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<div
style={{
width: 72,
height: 72,
borderRadius: "50%",
overflow: "hidden",
flexShrink: 0,
border: "2px solid var(--color-brand-200)",
background: "var(--color-surface-muted)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{driver.photoUrl && !avatarError ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={driver.photoUrl}
alt={driver.name}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
onError={() => setAvatarError(true)}
/>
) : (
<span style={{ fontSize: 28, fontWeight: 700, color: "var(--color-brand-600)" }}>
{driver.name.charAt(0)}
</span>
)}
</div>
<div>
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600, margin: 0 }}>
ملف السائق
</p>
<h1 style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
{driver.name}
</h1>
<div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 6, flexWrap: "wrap" }}>
{driver.userName && (
<span style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--color-text-muted)" }}>
@{driver.userName}
</span>
)}
<span style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--color-text-muted)" }}>
{driver.phone}
</span>
{statusConfig && (
<span
style={{
borderRadius: "var(--radius-full)",
border: `1px solid ${statusConfig.border}`,
background: statusConfig.bg,
padding: "0.2rem 0.625rem",
fontSize: 11,
fontWeight: 700,
color: statusConfig.color,
display: "inline-flex",
alignItems: "center",
gap: 5,
}}
>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusConfig.dot, flexShrink: 0 }} />
{statusConfig.label}
</span>
)}
</div>
</div>
</div>
{/* Actions */}
<div style={{ display: "flex", gap: "0.75rem" }}>
<button
type="button"
onClick={() => setDeleteOpen(true)}
style={{
height: 40,
padding: "0 1.25rem",
borderRadius: "var(--radius-lg)",
border: "1px solid #FECACA",
background: "#FEF2F2",
fontSize: 13,
fontWeight: 700,
color: "#DC2626",
cursor: "pointer",
fontFamily: "var(--font-sans)",
}}
>
حذف السائق
</button>
<button
type="button"
onClick={() => setEditOpen(true)}
style={{
height: 40,
padding: "0 1.25rem",
borderRadius: "var(--radius-lg)",
border: "none",
background: "var(--color-brand-600)",
fontSize: 13,
fontWeight: 700,
color: "#FFF",
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: 8,
fontFamily: "var(--font-sans)",
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
تعديل السائق
</button>
</div>
</div>
</header>
{/* ── Content grid ── */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "1.5rem",
}}
>
{/* Personal Info */}
<SectionCard title="البيانات الشخصية">
<DetailRow label="الاسم الكامل" value={driver.name} />
<DetailRow label="رقم الجوال" value={driver.phone} mono />
<DetailRow label="البريد الإلكتروني" value={driver.email ?? "—"} />
<DetailRow label="العنوان" value={driver.address ?? "—"} />
<DetailRow label="الجنسية" value={driver.nationality ?? "—"} />
<DetailRow label="الفرع" value={driver.branch?.name ?? "—"} />
<DetailRow label="نوع السائق" value={driver.driverType ?? "—"} />
</SectionCard>
{/* ID & GOSI */}
<SectionCard title="الهوية والتأمينات">
<DetailRow
label="نوع الهوية"
value={driver.nationalIdType ? NATIONAL_ID_TYPE_MAP[driver.nationalIdType] : "—"}
/>
<DetailRow label="رقم الهوية" value={driver.nationalId ?? "—"} mono />
<DetailRow
label="انتهاء الهوية"
value={fmtDate(driver.nationalIdExpiry)}
warn={isExpiringSoon(driver.nationalIdExpiry)}
/>
<DetailRow label="رقم GOSI" value={driver.gosiNumber ?? "—"} mono />
</SectionCard>
{/* License */}
<SectionCard title="بيانات رخصة القيادة">
<DetailRow label="رقم الرخصة" value={driver.licenseNumber ?? "—"} mono />
<DetailRow label="نوع الرخصة" value={driver.licenseType ?? "—"} />
<DetailRow
label="انتهاء الرخصة"
value={fmtDate(driver.licenseExpiry)}
warn={isExpiringSoon(driver.licenseExpiry)}
/>
</SectionCard>
{/* Driver Card */}
<SectionCard title="بطاقة السائق">
<DetailRow label="رقم البطاقة" value={driver.driverCardNumber ?? "—"} mono />
<DetailRow
label="نوع البطاقة"
value={driver.driverCardType ? DRIVER_CARD_TYPE_MAP[driver.driverCardType] : "—"}
/>
<DetailRow
label="انتهاء البطاقة"
value={fmtDate(driver.driverCardExpiry)}
warn={isExpiringSoon(driver.driverCardExpiry)}
/>
</SectionCard>
{/* System Info */}
<SectionCard title="معلومات النظام">
<DetailRow label="اسم المستخدم" value={driver.userName ?? "—"} mono />
<DetailRow label="تاريخ الإضافة" value={fmtDate(driver.createdAt)} />
<DetailRow label="آخر تحديث" value={fmtDate(driver.updatedAt)} />
</SectionCard>
{/* Status History */}
{driver.statusHistory && driver.statusHistory.length > 0 && (
<SectionCard title="سجل الحالات">
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{driver.statusHistory.map((h) => {
const s = DRIVER_STATUS_MAP[h.status] ?? DRIVER_STATUS_MAP.Active;
return (
<div
key={h.id}
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
borderRadius: "var(--radius-md)",
border: `1px solid ${s.border}`,
background: s.bg,
padding: "0.5rem 0.875rem",
}}
>
<div>
<span style={{ fontSize: 12, fontWeight: 600, color: s.color }}>
{s.label}
</span>
{h.reason && (
<span style={{ fontSize: 11, color: "var(--color-text-muted)", marginRight: 8 }}>
{h.reason}
</span>
)}
</div>
<span style={{ fontSize: 11, color: "var(--color-text-muted)" }}>
{fmtDate(h.createdAt)}
</span>
</div>
);
})}
</div>
</SectionCard>
)}
</div>
{/* ── Photos section (full width) ── */}
<SectionCard title="الصور والمستندات">
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "1.25rem" }}>
<PhotoCard url={driver.photoUrl} label="صورة السائق" />
<PhotoCard url={driver.nationalPhotoUrl} label="صورة الهوية" />
<PhotoCard url={driver.driverCardPhotoUrl} label="صورة بطاقة السائق" />
</div>
</SectionCard>
{/* ── Report section (full width) ── */}
<div
style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
boxShadow: "var(--shadow-card)",
padding: "1.5rem",
}}
>
<DriverReportPanel driverId={driver.id} />
</div>
</section>
{/* ── Edit modal ── */}
{editOpen && (
<DriverFormModal
editDriver={driver}
branches={[]}
onClose={() => setEditOpen(false)}
onSubmit={handleEditSubmit}
/>
)}
{/* ── Delete modal ── */}
{deleteOpen && (
<DriverDeleteModal
driver={driver}
deleting={deleting}
onCancel={() => setDeleteOpen(false)}
onConfirm={handleConfirmDelete}
/>
)}
</>
);
}

View File

@@ -0,0 +1,378 @@
"use client";
import { useState, useCallback } from "react";
import { Alert, Spinner } from "@/Components/UI";
import { DriverFormModal } from "@/Components/Driver/DriverFormModal";
import { DriverDeleteModal } from "@/Components/Driver/DriverDeleteModal";
import { driverService } from "@/services";
import { getStoredToken } from "@/lib/auth";
import { useDrivers } from "@/hooks/useDriver";
import { CreateDriverPayload, Driver, DRIVER_STATUS_MAP, UpdateDriverPayload } from "@/types/driver";
import { DriverDetailPanel } from "@/Components/Driver/DriverDetailPanel";
// ── Helpers ───────────────────────────────────────────────────────────────────
function fmtDate(iso?: string | null): string {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("ar-SA", {
year: "numeric", month: "short", day: "numeric",
});
}
function expirySoon(iso?: string | null): boolean {
if (!iso) return false;
return (new Date(iso).getTime() - Date.now()) / 86_400_000 <= 90;
}
// ── Styles ────────────────────────────────────────────────────────────────────
const cardStyle: React.CSSProperties = {
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
overflow: "hidden",
boxShadow: "var(--shadow-card)",
};
const thStyle: React.CSSProperties = {
padding: "0.75rem 1.5rem",
fontSize: 11,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.2em",
color: "var(--color-text-muted)",
background: "var(--color-surface-muted)",
borderBottom: "1px solid var(--color-border)",
};
// ── Page Component ────────────────────────────────────────────────────────────
export default function DriversPage() {
const {
drivers, loading, error, total, pages, page,
search, setPage, handleSearch, clearError,
deleteDriver, notification, reload,
} = useDrivers();
// ── Panel / modal state ───────────────────────────────────────────────────
const [selectedDriverId, setSelectedDriverId] = useState<string | null>(null);
const [formDriver, setFormDriver] = useState<Driver | null | "new">(null);
const [deleteTarget, setDeleteTarget] = useState<Driver | null>(null);
const [deleting, setDeleting] = useState(false);
// ── Handlers ──────────────────────────────────────────────────────────────
const handleEdit = useCallback((driver: Driver) => {
setSelectedDriverId(null);
setFormDriver(driver);
}, []);
const handleDelete = useCallback((driver: Driver) => {
setSelectedDriverId(null);
setDeleteTarget(driver);
}, []);
const handleFormSubmit = useCallback(
async (
payload: CreateDriverPayload | UpdateDriverPayload,
isNew: boolean,
): Promise<boolean> => {
try {
const token = getStoredToken();
if (isNew) {
// Use multipart if there are file fields
const hasFiles =
(payload as CreateDriverPayload & { photo?: File }).photo ||
(payload as CreateDriverPayload & { nationalPhoto?: File }).nationalPhoto ||
(payload as CreateDriverPayload & { driverCardPhoto?: File }).driverCardPhoto;
if (hasFiles) {
await driverService.createWithImages(
payload as Parameters<typeof driverService.createWithImages>[0],
token,
);
} else {
await driverService.create(payload as CreateDriverPayload, token);
}
} else {
const id = (formDriver as Driver).id;
await driverService.update(id, payload as UpdateDriverPayload, token);
}
reload();
return true;
} catch {
return false;
}
},
[formDriver, reload],
);
const handleConfirmDelete = useCallback(async () => {
if (!deleteTarget) return;
setDeleting(true);
const ok = await deleteDriver(deleteTarget.id);
setDeleting(false);
if (ok) setDeleteTarget(null);
}, [deleteTarget, deleteDriver]);
// ── Render ────────────────────────────────────────────────────────────────
return (
<>
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* ── Header ── */}
<header style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
padding: "1.5rem 2rem",
boxShadow: "var(--shadow-card)",
}}>
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600 }}>
إدارة الكوادر
</p>
<div style={{ marginTop: "0.75rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
<div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", flexWrap: "wrap", gap: "1rem" }}>
<div>
<h1 style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
السائقون
</h1>
<p style={{ marginTop: "0.25rem", fontSize: 13, color: "var(--color-text-muted)" }}>
إجمالي{" "}
<strong style={{ color: "var(--color-text-primary)" }}>{total}</strong>{" "}
سائق مسجل
</p>
</div>
<div style={{ display: "flex", gap: "0.75rem", alignItems: "center", flexWrap: "wrap" }}>
{/* Search */}
<div style={{ position: "relative", width: 288 }}>
<svg
style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", width: 16, height: 16, color: "var(--color-text-hint)", pointerEvents: "none" }}
fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"
>
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
placeholder="بحث بالاسم أو الهاتف..."
value={search}
onChange={(e) => handleSearch(e.target.value)}
dir="rtl"
style={{
width: "100%", height: 40, paddingRight: 36, paddingLeft: 12,
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13, color: "var(--color-text-primary)",
outline: "none", fontFamily: "var(--font-sans)",
}}
/>
</div>
{/* Add button */}
<button
type="button"
onClick={() => setFormDriver("new")}
style={{
height: 40, padding: "0 1.25rem",
borderRadius: "var(--radius-lg)",
border: "none",
background: "var(--color-brand-600)",
fontSize: 13, fontWeight: 700, color: "#FFF",
cursor: "pointer",
display: "flex", alignItems: "center", gap: 8,
fontFamily: "var(--font-sans)",
flexShrink: 0,
}}
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 5v14M5 12h14" />
</svg>
إضافة سائق
</button>
</div>
</div>
</div>
</header>
{/* ── Notifications ── */}
{notification && (
<Alert
type={notification.type}
message={notification.message}
/>
)}
{error && (
<Alert type="error" message={error} onClose={clearError} />
)}
{/* ── Table ── */}
<div style={cardStyle}>
<div
dir="rtl"
style={{
display: "grid",
gridTemplateColumns: "2fr 1.2fr 1fr 1.1fr 1.1fr 1fr",
...thStyle,
}}
>
<span>السائق</span>
<span>الفرع</span>
<span>الجنسية</span>
<span>انتهاء الرخصة</span>
<span>انتهاء الهوية</span>
<span>الحالة</span>
</div>
{loading ? (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "4rem 0", color: "var(--color-text-muted)" }}>
<Spinner size="sm" className="text-blue-600" />
<span style={{ fontSize: 13 }}>جارٍ التحميل</span>
</div>
) : drivers.length === 0 ? (
<p style={{ textAlign: "center", padding: "4rem 0", fontSize: 13, color: "var(--color-text-muted)" }}>
لا توجد نتائج {search && `لـ "${search}"`}
</p>
) : (
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
{drivers.map((d, i) => {
const statusCfg = DRIVER_STATUS_MAP[d.status] ?? DRIVER_STATUS_MAP.Inactive;
const licWarn = expirySoon(d.licenseExpiry);
const idWarn = expirySoon((d as Driver & { nationalIdExpiry?: string }).nationalIdExpiry);
return (
<li
key={d.id}
onClick={() => setSelectedDriverId(d.id)}
style={{
display: "grid",
gridTemplateColumns: "2fr 1.2fr 1fr 1.1fr 1.1fr 1fr",
alignItems: "center", gap: "0.5rem",
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent",
fontSize: 13,
cursor: "pointer",
transition: "background 0.15s",
}}
onMouseEnter={(e) => (e.currentTarget.style.background = "var(--color-surface-hover, #F8FAFC)")}
onMouseLeave={(e) => (e.currentTarget.style.background = i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent")}
>
{/* Name + phone */}
<div>
<p style={{ fontWeight: 600, color: "var(--color-text-primary)", margin: 0 }}>{d.name}</p>
<p style={{ marginTop: 2, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-muted)" }}>{d.phone}</p>
</div>
{/* Branch */}
<span style={{ color: "var(--color-text-secondary)" }}>{d.branch?.name ?? "—"}</span>
{/* Nationality */}
<span style={{ color: "var(--color-text-secondary)" }}>{d.nationality ?? "—"}</span>
{/* License expiry */}
<span style={{ fontSize: 12, fontWeight: licWarn ? 600 : 400, color: licWarn ? "#D97706" : "var(--color-text-secondary)" }}>
{licWarn && "⚠ "}{fmtDate(d.licenseExpiry)}
</span>
{/* National ID expiry */}
<span style={{ fontSize: 12, fontWeight: idWarn ? 600 : 400, color: idWarn ? "#D97706" : "var(--color-text-secondary)" }}>
{idWarn && "⚠ "}{fmtDate((d as Driver & { nationalIdExpiry?: string }).nationalIdExpiry)}
</span>
{/* Status badge */}
<span style={{
display: "inline-flex", alignItems: "center", gap: 5,
borderRadius: "var(--radius-full)",
border: `1px solid ${statusCfg.border}`,
background: statusCfg.bg,
padding: "0.2rem 0.625rem",
fontSize: 11, fontWeight: 600, color: statusCfg.color,
width: "fit-content",
}}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusCfg.dot, flexShrink: 0 }} />
{statusCfg.label}
</span>
</li>
);
})}
</ul>
)}
{/* ── Pagination ── */}
{pages > 1 && (
<div
dir="rtl"
style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
borderTop: "1px solid var(--color-border)", padding: "0.875rem 1.5rem",
}}
>
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
صفحة{" "}
<strong style={{ color: "var(--color-text-primary)" }}>{page}</strong>{" "}
من{" "}
<strong style={{ color: "var(--color-text-primary)" }}>{pages}</strong>
</span>
<div style={{ display: "flex", gap: "0.5rem" }}>
{[
{ label: "السابق", action: () => setPage(p => Math.max(1, p - 1)), disabled: page === 1 },
{ label: "التالي", action: () => setPage(p => Math.min(pages, p + 1)), disabled: page === pages },
].map(btn => (
<button
key={btn.label}
onClick={btn.action}
disabled={btn.disabled}
style={{
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
padding: "0.375rem 0.875rem",
fontSize: 12, color: "var(--color-text-secondary)",
cursor: btn.disabled ? "not-allowed" : "pointer",
opacity: btn.disabled ? 0.4 : 1,
fontFamily: "var(--font-sans)",
}}
>
{btn.label}
</button>
))}
</div>
</div>
)}
</div>
</section>
{/* ── Detail panel ── */}
{selectedDriverId && (
<DriverDetailPanel
driverId={selectedDriverId}
onClose={() => setSelectedDriverId(null)}
onEdit={handleEdit}
onDelete={handleDelete}
/>
)}
{/* ── Form modal ── */}
{formDriver !== null && (
<DriverFormModal
editDriver={formDriver === "new" ? null : formDriver}
branches={[]}
onClose={() => setFormDriver(null)}
onSubmit={handleFormSubmit}
/>
)}
{/* ── Delete modal ── */}
{deleteTarget && (
<DriverDeleteModal
driver={deleteTarget}
deleting={deleting}
onCancel={() => setDeleteTarget(null)}
onConfirm={handleConfirmDelete}
/>
)}
</>
);
}