add archive layout pages to user car driver trip order
This commit is contained in:
301
src/Components/Driver/archive/ArchivedDriverDetailModal.tsx
Normal file
301
src/Components/Driver/archive/ArchivedDriverDetailModal.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Spinner } from "../../UI";
|
||||
import { PhotoCard } from "../DriverPhotos";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
import { archivedDriverService } from "@/src/services/archive/archivedDriver.service";
|
||||
import {
|
||||
DRIVER_STATUS_MAP,
|
||||
DRIVER_CARD_TYPE_MAP,
|
||||
NATIONAL_ID_TYPE_MAP,
|
||||
} from "@/src/types/driver";
|
||||
import type { ArchivedDriver, DriverStatusHistoryEntry } from "@/src/types/driver";
|
||||
|
||||
interface ArchivedDriverDetailModalProps {
|
||||
driverId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// ── small helper components ───────────────────────────────────────────────────
|
||||
|
||||
/** Renders a single label/value row inside the detail body. */
|
||||
function DetailRow({ label, value }: { label: string; value?: string | null }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex", flexDirection: "column", gap: 4,
|
||||
padding: "0.75rem 0",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
}}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.15em", color: "var(--color-text-muted)" }}>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: value ? "var(--color-text-primary)" : "var(--color-text-hint)" }}>
|
||||
{value || "—"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Avatar({ name }: { name: string }) {
|
||||
const initials = name.trim().split(" ").slice(0, 2).map(w => w[0]).join("").toUpperCase();
|
||||
return (
|
||||
<div style={{
|
||||
width: 64, height: 64, borderRadius: "50%",
|
||||
background: "linear-gradient(135deg, #EA580C 0%, #B91C1C 100%)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: 22, fontWeight: 700, color: "#FFF",
|
||||
flexShrink: 0,
|
||||
boxShadow: "0 4px 12px rgba(234,88,12,.3)",
|
||||
}}>
|
||||
{initials}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal showing the full profile of a single archived driver, including
|
||||
* documents/photos and status history. Fetches `getById` and
|
||||
* `getStatusHistory` in parallel on mount.
|
||||
*/
|
||||
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);
|
||||
|
||||
// close on Escape
|
||||
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 [driverRes, historyRes] = await Promise.all([
|
||||
archivedDriverService.getById(driverId, token),
|
||||
archivedDriverService.getStatusHistory(driverId, token),
|
||||
]);
|
||||
if (!cancelled) {
|
||||
setDriver(driverRes.data);
|
||||
setHistory(historyRes.data ?? []);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setError("تعذّر تحميل بيانات السائق المؤرشف. يرجى المحاولة لاحقاً.");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [driverId]);
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
const fmt = (iso?: string | null) =>
|
||||
iso ? new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "long", day: "numeric" }) : null;
|
||||
|
||||
const statusConfig = driver ? DRIVER_STATUS_MAP[driver.status] : null;
|
||||
|
||||
// ── 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",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* ── body ── */}
|
||||
<div style={{ padding: "1.5rem", overflowY: "auto", maxHeight: "75vh" }}>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
141
src/Components/Driver/archive/ArchivedDriverTable.tsx
Normal file
141
src/Components/Driver/archive/ArchivedDriverTable.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "../../UI";
|
||||
import { DRIVER_STATUS_MAP } from "@/src/types/driver";
|
||||
import type { ArchivedDriver } from "@/src/types/driver";
|
||||
|
||||
// ── status badge ─────────────────────────────────────────────────────────────
|
||||
function StatusBadge({ status }: { status: ArchivedDriver["status"] }) {
|
||||
const cfg = DRIVER_STATUS_MAP[status] ?? DRIVER_STATUS_MAP.Inactive;
|
||||
return (
|
||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 5, borderRadius: "var(--radius-full)", border: `1px solid ${cfg.border}`, background: cfg.bg, padding: "0.2rem 0.625rem", fontSize: 11, fontWeight: 600, color: cfg.color }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: cfg.dot }} />
|
||||
{cfg.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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)",
|
||||
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)",
|
||||
};
|
||||
|
||||
const ROW_GRID_COLUMNS = "2fr 1.5fr 1fr 1fr 1fr 100px";
|
||||
|
||||
// ── Props ────────────────────────────────────────────────────────────────────
|
||||
interface ArchivedDriverTableProps {
|
||||
drivers: ArchivedDriver[];
|
||||
loading: boolean;
|
||||
search: string;
|
||||
page: number;
|
||||
pages: number;
|
||||
onView: (driver: ArchivedDriver) => void;
|
||||
onPageChange: (p: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the paginated table of archived drivers, including loading,
|
||||
* empty, and error-free states. Mirrors ArchivedUserTable for consistency.
|
||||
*/
|
||||
export function ArchivedDriverTable({ drivers, loading, search, page, pages, onView, onPageChange }: ArchivedDriverTableProps) {
|
||||
return (
|
||||
<div style={cardStyle}>
|
||||
{/* column headers */}
|
||||
<div dir="rtl" style={{ display: "grid", gridTemplateColumns: ROW_GRID_COLUMNS, ...thStyle }}>
|
||||
<span>الاسم</span>
|
||||
<span>اسم المستخدم</span>
|
||||
<span>الحالة</span>
|
||||
<span style={{ textAlign: "center" }}>تاريخ الإنشاء</span>
|
||||
<span style={{ textAlign: "center" }}>آخر تحديث</span>
|
||||
<span style={{ textAlign: "center" }}>إجراءات</span>
|
||||
</div>
|
||||
|
||||
{/* loading state */}
|
||||
{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 ? (
|
||||
/* empty state */
|
||||
<div style={{ textAlign: "center", padding: "4rem 1rem" }}>
|
||||
<p style={{ fontSize: 13, color: "var(--color-text-muted)" }}>
|
||||
{search ? `لا توجد نتائج لـ "${search}"` : "لا يوجد سائقون في الأرشيف."}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* data rows */
|
||||
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
||||
{drivers.map((d, i) => (
|
||||
<li key={d.id} onClick={() => onView(d)} style={{
|
||||
display: "grid", gridTemplateColumns: ROW_GRID_COLUMNS,
|
||||
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",
|
||||
}}>
|
||||
<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>
|
||||
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>{d.userName ?? "—"}</span>
|
||||
<StatusBadge status={d.status} />
|
||||
<span style={{ textAlign: "center", fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||
{new Date(d.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })}
|
||||
</span>
|
||||
<span style={{ textAlign: "center", fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||
{new Date(d.updatedAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })}
|
||||
</span>
|
||||
<div style={{ display: "flex", justifyContent: "center", gap: 4 }}>
|
||||
{/* view only — delete/restore not implemented yet, same as users */}
|
||||
<IconBtn title={`عرض ${d.name}`} color="#059669" bg="#ECFDF5" borderColor="#A7F3D0" onClick={() => onView(d)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</IconBtn>
|
||||
</div>
|
||||
</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: () => 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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
119
src/Components/Driver/archive/ArchivedDrivers.tsx
Normal file
119
src/Components/Driver/archive/ArchivedDrivers.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Alert } from "../../UI";
|
||||
import { ArchivedDriverTable } from "./ArchivedDriverTable";
|
||||
import { ArchivedDriverDetailModal } from "./ArchivedDriverDetailModal";
|
||||
import { useArchivedDrivers } from "@/src/hooks/archive/useArchivedDrivers";
|
||||
|
||||
interface ArchivedDriversProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level modal for browsing archived drivers. Wires the
|
||||
* useArchivedDrivers hook to the table and detail modal, mirroring
|
||||
* ArchivedUsersModal's structure and behavior for drivers.
|
||||
*/
|
||||
export function ArchivedDrivers({ onClose }: ArchivedDriversProps) {
|
||||
const [viewDriverId, setViewDriverId] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
drivers, loading, total, pages, error,
|
||||
page, search,
|
||||
setPage, handleSearch, clearError,
|
||||
} = useArchivedDrivers();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog" aria-modal="true" aria-labelledby="archive-driver-modal-title"
|
||||
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||
style={{
|
||||
position: "fixed", inset: 0, zIndex: 70,
|
||||
background: "rgba(15,23,42,0.6)", backdropFilter: "blur(4px)",
|
||||
display: "flex", alignItems: "flex-start", justifyContent: "center",
|
||||
padding: "2rem 1rem", overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{viewDriverId && (
|
||||
<ArchivedDriverDetailModal driverId={viewDriverId} onClose={() => setViewDriverId(null)} />
|
||||
)}
|
||||
|
||||
<div
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{
|
||||
width: "100%", maxWidth: 920,
|
||||
background: "var(--color-surface-sunken)",
|
||||
borderRadius: "var(--radius-xl)",
|
||||
border: "1px solid var(--color-border)",
|
||||
boxShadow: "0 24px 64px rgba(0,0,0,.2)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* header */}
|
||||
<div dir="rtl" style={{
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
padding: "1.25rem 1.5rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
}}>
|
||||
<div>
|
||||
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>
|
||||
الأرشيف
|
||||
</p>
|
||||
<h2 id="archive-driver-modal-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||
السائقون المؤرشفون
|
||||
<span style={{ marginInlineStart: 8, fontSize: 13, fontWeight: 500, color: "var(--color-text-muted)" }}>
|
||||
({total})
|
||||
</span>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* body */}
|
||||
<div style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||
{/* search */}
|
||||
<div dir="rtl" style={{ position: "relative", maxWidth: 320 }}>
|
||||
<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, outline: "none",
|
||||
fontFamily: "var(--font-sans)",
|
||||
color: "var(--color-text-primary)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <Alert type="error" message={error} onClose={clearError} />}
|
||||
|
||||
<ArchivedDriverTable
|
||||
drivers={drivers}
|
||||
loading={loading}
|
||||
search={search}
|
||||
page={page}
|
||||
pages={pages}
|
||||
onView={driver => setViewDriverId(driver.id)}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user