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:
m7amedez5511
2026-07-22 14:19:32 +03:00
parent f1a521dd5a
commit b87d81100f
59 changed files with 2261 additions and 3261 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
);
}

View File

@@ -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>
)}

View File

@@ -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}