The trip files have been reorganized. Errors related to vehicle and driver editing have been resolved, archive display issues fixed, and model data display standardized across the project; order and trip-related issues have also been addressed.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Spinner } from "../UI";
|
||||
import { Alert, Button, Spinner } from "../UI";
|
||||
import { driverService } from "@/src/services/driver.service";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
import { DriverReportPanel } from "../Driver_Report/driverReport";
|
||||
@@ -306,18 +306,7 @@ export function DriverDetailPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
borderRadius: "var(--radius-md)",
|
||||
background: "#FEF2F2",
|
||||
border: "1px solid #FECACA",
|
||||
padding: "0.75rem 1rem",
|
||||
fontSize: 13,
|
||||
color: "#DC2626",
|
||||
}}>
|
||||
⚠ {error}
|
||||
</div>
|
||||
)}
|
||||
{error && <Alert type="error" message={error} />}
|
||||
|
||||
{driver && !loading && (
|
||||
<>
|
||||
@@ -462,55 +451,21 @@ export function DriverDetailPanel({
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDelete(driver)}
|
||||
style={{
|
||||
height: 40, padding: "0 1rem",
|
||||
borderRadius: "var(--radius-md)",
|
||||
border: "1px solid #FECACA",
|
||||
background: "#FEF2F2",
|
||||
fontSize: 13, fontWeight: 700, color: "#DC2626",
|
||||
cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
<Button type="button" variant="danger" onClick={() => onDelete(driver)}>
|
||||
حذف
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEdit(driver)}
|
||||
style={{
|
||||
height: 40, padding: "0 1rem",
|
||||
borderRadius: "var(--radius-md)",
|
||||
border: "1px solid var(--color-brand-200)",
|
||||
background: "var(--color-brand-50, #EFF6FF)",
|
||||
fontSize: 13, fontWeight: 700, color: "var(--color-brand-600)",
|
||||
cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||
display: "flex", alignItems: "center", gap: 6,
|
||||
}}
|
||||
>
|
||||
<Button type="button" variant="secondary" onClick={() => onEdit(driver)}>
|
||||
<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>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
flex: 1, height: 40,
|
||||
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 type="button" variant="secondary" fullWidth onClick={onClose}>
|
||||
إغلاق
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
@@ -86,36 +86,26 @@ export function DriverFormModal({
|
||||
}: DriverFormModalProps) {
|
||||
const isNew = editDriver === null;
|
||||
|
||||
// ── Local branches (auto-fetched if prop is empty) ─────────────────────────
|
||||
const [branches, setBranches] = useState<Branch[]>(branchesProp);
|
||||
useEffect(() => {
|
||||
if (branchesProp.length > 0) {
|
||||
queueMicrotask(() => setBranches(branchesProp));
|
||||
return;
|
||||
}
|
||||
const token = getStoredToken();
|
||||
get<{ data: { data: Branch[] } }>("branches?limit=100", token)
|
||||
.then((res) => {
|
||||
const list = (res as unknown as { data: { data: Branch[] } }).data?.data ?? [];
|
||||
setBranches(list);
|
||||
})
|
||||
.catch(() => {
|
||||
/* silently ignore */
|
||||
});
|
||||
}, [branchesProp]);
|
||||
|
||||
// ── react-hook-form ────────────────────────────────────────────────────────
|
||||
// Moved above the branches-loading effect below: that effect calls
|
||||
// setValue(), so useForm() (which defines it) must run first — otherwise
|
||||
// setValue is referenced before its own initialization (TDZ ReferenceError),
|
||||
// same issue already fixed in TripFormModal.
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
setError,
|
||||
setFocus,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<DriverFormValues>({
|
||||
// Cast the schema itself — the create/update schemas have structurally
|
||||
// different required fields, so yup can't unify them into one type.
|
||||
resolver: yupResolver((isNew ? createDriverSchema : updateDriverSchema) as any),
|
||||
// Cast both the schema argument and the resolver's result — same fix
|
||||
// already applied in TripFormModal: createDriverSchema/updateDriverSchema
|
||||
// have structurally different required fields (union type yupResolver's
|
||||
// single-schema signature won't accept), and yup's inferred type for
|
||||
// .optional() fields is structurally incompatible with DriverFormValues.
|
||||
resolver: yupResolver((isNew ? createDriverSchema : updateDriverSchema) as any) as any,
|
||||
defaultValues: {
|
||||
name: editDriver?.name ?? "",
|
||||
phone: editDriver?.phone ?? "",
|
||||
@@ -142,6 +132,33 @@ export function DriverFormModal({
|
||||
},
|
||||
});
|
||||
|
||||
// ── Local branches (auto-fetched if prop is empty) ─────────────────────────
|
||||
const [branches, setBranches] = useState<Branch[]>(branchesProp);
|
||||
useEffect(() => {
|
||||
// defaultValues.branchId was applied before this list existed, so the
|
||||
// <select> had no matching <option> yet and silently fell back to "" —
|
||||
// reapply the saved id now that the option actually exists in the DOM.
|
||||
const savedBranchId = (editDriver as Driver & { branchId?: string })?.branchId;
|
||||
|
||||
if (branchesProp.length > 0) {
|
||||
queueMicrotask(() => {
|
||||
setBranches(branchesProp);
|
||||
if (savedBranchId) setValue("branchId", savedBranchId);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const token = getStoredToken();
|
||||
get<{ data: { data: Branch[] } }>("branches?limit=100", token)
|
||||
.then((res) => {
|
||||
const list = (res as unknown as { data: { data: Branch[] } }).data?.data ?? [];
|
||||
setBranches(list);
|
||||
if (savedBranchId) setValue("branchId", savedBranchId);
|
||||
})
|
||||
.catch(() => {
|
||||
/* silently ignore */
|
||||
});
|
||||
}, [branchesProp, editDriver, setValue]);
|
||||
|
||||
const [apiError, setApiError] = useState("");
|
||||
|
||||
// register("name") already attaches its own ref — focusing via setFocus
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Spinner } from "../../UI";
|
||||
import { Button, Modal, Spinner, Toast } from "../../UI";
|
||||
import { PhotoCard } from "../DriverPhotos";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
import { archivedDriverService } from "@/src/services/archive/archivedDriver.service";
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
NATIONAL_ID_TYPE_MAP,
|
||||
} from "@/src/types/driver";
|
||||
import type { ArchivedDriver, DriverStatusHistoryEntry } from "@/src/types/driver";
|
||||
import type { ToastNotification } from "@/src/Components/UI";
|
||||
|
||||
interface ArchivedDriverDetailModalProps {
|
||||
driverId: string;
|
||||
@@ -18,6 +19,7 @@ interface ArchivedDriverDetailModalProps {
|
||||
}
|
||||
|
||||
// ── small helper components ───────────────────────────────────────────────────
|
||||
// (No shared-UI equivalents — purpose-built layouts, not generic controls.)
|
||||
|
||||
/** Renders a single label/value row inside the detail body. */
|
||||
function DetailRow({ label, value }: { label: string; value?: string | null }) {
|
||||
@@ -55,44 +57,35 @@ function Avatar({ name }: { name: string }) {
|
||||
|
||||
/**
|
||||
* Modal showing the full profile of a single archived driver, including
|
||||
* documents/photos and status history. Fetches `getById` and
|
||||
* `getStatusHistory` in parallel on mount.
|
||||
* documents/photos and status history. Fetches `getById` on mount — the
|
||||
* archived detail response includes status history embedded.
|
||||
*/
|
||||
export function ArchivedDriverDetailModal({ driverId, onClose }: ArchivedDriverDetailModalProps) {
|
||||
const [driver, setDriver] = useState<ArchivedDriver | null>(null);
|
||||
const [history, setHistory] = useState<DriverStatusHistoryEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notification, setNotification] = useState<ToastNotification | null>(null);
|
||||
|
||||
// close on Escape
|
||||
// fetch archived driver details on mount — status history comes embedded
|
||||
// in the same response, no separate call needed
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [onClose]);
|
||||
|
||||
// fetch archived driver details + status history on mount
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
const [driverData, historyList] = await Promise.all([
|
||||
archivedDriverService.getByIdUnwrapped(driverId, token),
|
||||
archivedDriverService.getStatusHistoryUnwrapped(driverId, token),
|
||||
]);
|
||||
if (!cancelled) {
|
||||
setDriver(driverData);
|
||||
setHistory(historyList ?? []);
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
const driverData = await archivedDriverService.getByIdUnwrapped(driverId, token);
|
||||
if (!cancelled) {
|
||||
setDriver(driverData);
|
||||
setHistory(driverData.statusHistory ?? []);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setNotification({ type: "error", message: "تعذّر تحميل بيانات السائق المؤرشف. يرجى المحاولة لاحقاً." });
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setError("تعذّر تحميل بيانات السائق المؤرشف. يرجى المحاولة لاحقاً.");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [driverId]);
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [driverId]);
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
const fmt = (iso?: string | null) =>
|
||||
@@ -102,200 +95,124 @@ export function ArchivedDriverDetailModal({ driverId, onClose }: ArchivedDriverD
|
||||
|
||||
// ── render ────────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<div
|
||||
role="dialog" aria-modal="true" aria-labelledby="archived-driver-detail-title"
|
||||
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||
style={{
|
||||
position: "fixed", inset: 0, zIndex: 55,
|
||||
background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
|
||||
}}
|
||||
<Modal
|
||||
open
|
||||
title={driver?.name ?? "عرض السائق"}
|
||||
subtitle="سائق مؤرشف"
|
||||
onClose={onClose}
|
||||
zIndex={60}
|
||||
size="lg"
|
||||
footer={
|
||||
<Button type="button" variant="secondary" onClick={onClose}>
|
||||
إغلاق
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{
|
||||
width: "100%", maxWidth: 560,
|
||||
background: "var(--color-surface)",
|
||||
borderRadius: "var(--radius-2xl)",
|
||||
border: "1px solid var(--color-border)",
|
||||
boxShadow: "0 24px 64px rgba(0,0,0,.18)",
|
||||
overflow: "hidden",
|
||||
display: "flex", flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* ── header ── */}
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
padding: "1.25rem 1.5rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface-muted)",
|
||||
}}>
|
||||
<div>
|
||||
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>
|
||||
سائق مؤرشف
|
||||
</p>
|
||||
<h2 id="archived-driver-detail-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||
{driver?.name ?? "عرض السائق"}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button" onClick={onClose} aria-label="إغلاق"
|
||||
style={{
|
||||
width: 34, height: 34, borderRadius: "var(--radius-md)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
cursor: "pointer", fontSize: 18,
|
||||
color: "var(--color-text-muted)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{/* loading */}
|
||||
{loading && (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 10, padding: "3rem 0", color: "var(--color-text-muted)" }}>
|
||||
<Spinner size="sm" className="text-blue-600" />
|
||||
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── body ── */}
|
||||
<div style={{ padding: "1.5rem", overflowY: "auto", maxHeight: "75vh" }}>
|
||||
{/* error */}
|
||||
{!loading && <Toast notification={notification} onDismiss={() => setNotification(null)} />}
|
||||
|
||||
{/* loading */}
|
||||
{loading && (
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 10, padding: "3rem 0", color: "var(--color-text-muted)" }}>
|
||||
<Spinner size="sm" className="text-blue-600" />
|
||||
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||
</div>
|
||||
)}
|
||||
{/* content */}
|
||||
{!loading && driver && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }} dir="rtl">
|
||||
|
||||
{/* error */}
|
||||
{!loading && error && (
|
||||
<div style={{
|
||||
padding: "1rem 1.25rem",
|
||||
borderRadius: "var(--radius-lg)",
|
||||
background: "#FEF2F2", border: "1px solid #FECACA",
|
||||
fontSize: 13, color: "#991B1B", fontWeight: 500,
|
||||
textAlign: "center",
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* content */}
|
||||
{!loading && driver && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }} dir="rtl">
|
||||
|
||||
{/* avatar + name + status row */}
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", gap: "1rem",
|
||||
padding: "0 0 1.25rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
marginBottom: "0.25rem",
|
||||
}}>
|
||||
<Avatar name={driver.name} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p style={{ fontSize: 16, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>{driver.name}</p>
|
||||
{driver.userName && (
|
||||
<p style={{ marginTop: 3, fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>
|
||||
@{driver.userName}
|
||||
</p>
|
||||
)}
|
||||
{statusConfig && (
|
||||
<span style={{
|
||||
marginTop: 8, display: "inline-flex", alignItems: "center", gap: 6,
|
||||
borderRadius: "var(--radius-full)",
|
||||
border: `1px solid ${statusConfig.border}`,
|
||||
background: statusConfig.bg,
|
||||
padding: "0.25rem 0.75rem",
|
||||
fontSize: 12, fontWeight: 600, color: statusConfig.color,
|
||||
}}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusConfig.dot }} />
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* core info */}
|
||||
<DetailRow label="رقم الجوال" value={driver.phone} />
|
||||
<DetailRow label="البريد الإلكتروني" value={driver.email} />
|
||||
<DetailRow label="العنوان" value={driver.address} />
|
||||
<DetailRow label="الجنسية" value={driver.nationality} />
|
||||
<DetailRow label="نوع الهوية" value={driver.nationalIdType ? NATIONAL_ID_TYPE_MAP[driver.nationalIdType] : null} />
|
||||
<DetailRow label="رقم الهوية" value={driver.nationalId} />
|
||||
<DetailRow label="رقم GOSI" value={driver.gosiNumber} />
|
||||
<DetailRow label="رقم الرخصة" value={driver.licenseNumber} />
|
||||
<DetailRow label="نوع الرخصة" value={driver.licenseType} />
|
||||
<DetailRow label="انتهاء الرخصة" value={fmt(driver.licenseExpiry)} />
|
||||
<DetailRow label="رقم بطاقة السائق" value={driver.driverCardNumber} />
|
||||
<DetailRow label="نوع بطاقة السائق" value={driver.driverCardType ? DRIVER_CARD_TYPE_MAP[driver.driverCardType] : null} />
|
||||
<DetailRow label="نوع السائق" value={driver.driverType} />
|
||||
<DetailRow label="تاريخ الإنشاء" value={fmt(driver.createdAt)} />
|
||||
<DetailRow label="آخر تحديث" value={fmt(driver.updatedAt)} />
|
||||
|
||||
{/* photos */}
|
||||
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "1.25rem 0 0.5rem" }}>
|
||||
الصور والمستندات
|
||||
</p>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
|
||||
<PhotoCard url={driver.photoUrl} label="صورة السائق" />
|
||||
<PhotoCard url={driver.nationalPhotoUrl} label="صورة الهوية" />
|
||||
<PhotoCard url={driver.driverCardPhotoUrl} label="صورة البطاقة" />
|
||||
</div>
|
||||
|
||||
{/* status history */}
|
||||
{history.length > 0 && (
|
||||
<>
|
||||
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "1.25rem 0 0.5rem" }}>
|
||||
سجل الحالات
|
||||
</p>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{history.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)" }}>{fmt(h.createdAt)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
{/* avatar + name + status row */}
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", gap: "1rem",
|
||||
padding: "0 0 1.25rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
marginBottom: "0.25rem",
|
||||
}}>
|
||||
<Avatar name={driver.name} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p style={{ fontSize: 16, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>{driver.name}</p>
|
||||
{driver.userName && (
|
||||
<p style={{ marginTop: 3, fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>
|
||||
@{driver.userName}
|
||||
</p>
|
||||
)}
|
||||
{statusConfig && (
|
||||
<span style={{
|
||||
marginTop: 8, display: "inline-flex", alignItems: "center", gap: 6,
|
||||
borderRadius: "var(--radius-full)",
|
||||
border: `1px solid ${statusConfig.border}`,
|
||||
background: statusConfig.bg,
|
||||
padding: "0.25rem 0.75rem",
|
||||
fontSize: 12, fontWeight: 600, color: statusConfig.color,
|
||||
}}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusConfig.dot }} />
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* core info */}
|
||||
<DetailRow label="رقم الجوال" value={driver.phone} />
|
||||
<DetailRow label="البريد الإلكتروني" value={driver.email} />
|
||||
<DetailRow label="العنوان" value={driver.address} />
|
||||
<DetailRow label="الجنسية" value={driver.nationality} />
|
||||
<DetailRow label="نوع الهوية" value={driver.nationalIdType ? NATIONAL_ID_TYPE_MAP[driver.nationalIdType] : null} />
|
||||
<DetailRow label="رقم الهوية" value={driver.nationalId} />
|
||||
<DetailRow label="رقم GOSI" value={driver.gosiNumber} />
|
||||
<DetailRow label="رقم الرخصة" value={driver.licenseNumber} />
|
||||
<DetailRow label="نوع الرخصة" value={driver.licenseType} />
|
||||
<DetailRow label="انتهاء الرخصة" value={fmt(driver.licenseExpiry)} />
|
||||
<DetailRow label="رقم بطاقة السائق" value={driver.driverCardNumber} />
|
||||
<DetailRow label="نوع بطاقة السائق" value={driver.driverCardType ? DRIVER_CARD_TYPE_MAP[driver.driverCardType] : null} />
|
||||
<DetailRow label="نوع السائق" value={driver.driverType} />
|
||||
<DetailRow label="تاريخ الإنشاء" value={fmt(driver.createdAt)} />
|
||||
<DetailRow label="آخر تحديث" value={fmt(driver.updatedAt)} />
|
||||
|
||||
{/* photos */}
|
||||
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "1.25rem 0 0.5rem" }}>
|
||||
الصور والمستندات
|
||||
</p>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
|
||||
<PhotoCard url={driver.photoUrl} label="صورة السائق" />
|
||||
<PhotoCard url={driver.nationalPhotoUrl} label="صورة الهوية" />
|
||||
<PhotoCard url={driver.driverCardPhotoUrl} label="صورة البطاقة" />
|
||||
</div>
|
||||
|
||||
{/* status history */}
|
||||
{history.length > 0 && (
|
||||
<>
|
||||
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "1.25rem 0 0.5rem" }}>
|
||||
سجل الحالات
|
||||
</p>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{history.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)" }}>{fmt(h.createdAt)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── footer ── */}
|
||||
<div style={{
|
||||
padding: "1rem 1.5rem",
|
||||
borderTop: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface-muted)",
|
||||
display: "flex", justifyContent: "flex-end",
|
||||
}}>
|
||||
<button
|
||||
type="button" onClick={onClose}
|
||||
style={{
|
||||
height: 40, padding: "0 1.5rem",
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "../../UI";
|
||||
import { Button, EmptyState, IconBtn, Spinner } from "../../UI";
|
||||
import { DRIVER_STATUS_MAP } from "@/src/types/driver";
|
||||
import type { ArchivedDriver } from "@/src/types/driver";
|
||||
|
||||
// ── status badge ─────────────────────────────────────────────────────────────
|
||||
// Kept custom rather than swapped for <Badge/>: colors come dynamically from
|
||||
// DRIVER_STATUS_MAP (multiple statuses, each with its own color+dot), which
|
||||
// the shared Badge component's fixed palette doesn't cover.
|
||||
function StatusBadge({ status }: { status: ArchivedDriver["status"] }) {
|
||||
const cfg = DRIVER_STATUS_MAP[status] ?? DRIVER_STATUS_MAP.Inactive;
|
||||
return (
|
||||
@@ -15,16 +18,6 @@ function StatusBadge({ status }: { status: ArchivedDriver["status"] }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ── icon button ──────────────────────────────────────────────────────────────
|
||||
function IconBtn({ onClick, title, color, bg, borderColor, children }: { onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<button type="button" title={title} aria-label={title} onClick={e => { e.stopPropagation(); onClick(); }}
|
||||
style={{ width: 32, height: 32, borderRadius: "var(--radius-md)", border: `1px solid ${borderColor}`, background: bg, color, cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", transition: "opacity 150ms" }}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ── card / header styles ─────────────────────────────────────────────────────
|
||||
const cardStyle: React.CSSProperties = {
|
||||
borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)",
|
||||
@@ -75,11 +68,10 @@ export function ArchivedDriverTable({ drivers, loading, search, page, pages, onV
|
||||
</div>
|
||||
) : drivers.length === 0 ? (
|
||||
/* empty state */
|
||||
<div style={{ textAlign: "center", padding: "4rem 1rem" }}>
|
||||
<p style={{ fontSize: 13, color: "var(--color-text-muted)" }}>
|
||||
{search ? `لا توجد نتائج لـ "${search}"` : "لا يوجد سائقون في الأرشيف."}
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon="🗄️"
|
||||
title={search ? `لا توجد نتائج لـ "${search}"` : "لا يوجد سائقون في الأرشيف."}
|
||||
/>
|
||||
) : (
|
||||
/* data rows */
|
||||
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
||||
@@ -124,15 +116,24 @@ export function ArchivedDriverTable({ drivers, loading, search, page, pages, onV
|
||||
صفحة <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: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 },
|
||||
{ label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages },
|
||||
].map(btn => (
|
||||
<button key={btn.label} type="button" 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>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(Math.max(1, page - 1))}
|
||||
disabled={page === 1}
|
||||
>
|
||||
السابق
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(Math.min(pages, page + 1))}
|
||||
disabled={page === pages}
|
||||
>
|
||||
التالي
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Alert, Input, Modal } from "../../UI";
|
||||
import { Input, Modal, Toast } from "../../UI";
|
||||
import { ArchivedDriverTable } from "./ArchivedDriverTable";
|
||||
import { ArchivedDriverDetailModal } from "./ArchivedDriverDetailModal";
|
||||
import { useArchivedDrivers } from "@/src/hooks/archive/useArchivedDrivers";
|
||||
@@ -34,9 +34,9 @@ export function ArchivedDrivers({ onClose }: ArchivedDriversProps) {
|
||||
const [viewDriverId, setViewDriverId] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
drivers, loading, total, pages, error,
|
||||
drivers, loading, total, pages, notification,
|
||||
page, search,
|
||||
setPage, handleSearch, clearError,
|
||||
setPage, handleSearch, clearNotification,
|
||||
} = useArchivedDrivers();
|
||||
|
||||
return (
|
||||
@@ -65,7 +65,7 @@ export function ArchivedDrivers({ onClose }: ArchivedDriversProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <Alert type="error" message={error} onClose={clearError} />}
|
||||
<Toast notification={notification} onDismiss={clearNotification} />
|
||||
|
||||
<ArchivedDriverTable
|
||||
drivers={drivers}
|
||||
|
||||
Reference in New Issue
Block a user