Replace SVGs with icons, restructure the table for CRUD operations across the project, standardize the application's display, and build a new template for the table's action buttons.
This commit is contained in:
@@ -120,18 +120,7 @@ function PhotoCard({ url, label }: { url?: string | null; label: string }) {
|
||||
onError={() => setImgError(true)}
|
||||
/>
|
||||
) : (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
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>
|
||||
<i className="ti ti-photo" style={{ fontSize: 28, color: "var(--color-text-hint)" }} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
{url && !imgError && (
|
||||
@@ -456,10 +445,7 @@ export function DriverDetailPanel({
|
||||
</Button>
|
||||
|
||||
<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>
|
||||
<i className="ti ti-edit" style={{ fontSize: 14 }} aria-hidden="true" />
|
||||
تعديل
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -79,18 +79,7 @@ export function PhotoCard({
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<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>
|
||||
<i className="ti ti-photo" style={{ fontSize: 32, color: "var(--color-text-hint)" }} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
{url && !imgError && (
|
||||
@@ -110,4 +99,4 @@ export function PhotoCard({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
146
src/Components/Driver/DriverTable.tsx
Normal file
146
src/Components/Driver/DriverTable.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
// src/Components/Driver/DriverTable.tsx
|
||||
// NEW — previously this table was written inline inside
|
||||
// app/dashboard/drivers/page.tsx (the only model whose table wasn't
|
||||
// already split into its own component). Extracted here using
|
||||
// ReusableTable + ActionButtons, matching every other *Table.tsx in the
|
||||
// project. Same columns, same warn-highlighting for expiring
|
||||
// license/national-ID dates, same row-click-to-view / edit+delete icons
|
||||
// behavior as the original inline version.
|
||||
//
|
||||
// Integration: in app/dashboard/drivers/page.tsx, replace the entire
|
||||
// `<div style={cardStyle}>...</div>` table block (and its `fmtDate` /
|
||||
// `expirySoon` / `cardStyle` / `thStyle` / `ROW_GRID_COLUMNS` /
|
||||
// `iconBtnBase` helpers, now unused) with:
|
||||
//
|
||||
// <DriverTable
|
||||
// drivers={drivers}
|
||||
// loading={loading}
|
||||
// search={search}
|
||||
// page={page}
|
||||
// pages={pages}
|
||||
// onPageChange={setPage}
|
||||
// onRowClick={(d) => setSelectedDriverId(d.id)}
|
||||
// onEdit={handleEdit}
|
||||
// onDelete={handleDelete}
|
||||
// />
|
||||
|
||||
import { ReusableTable, ActionButtons } from "../UI";
|
||||
import { DRIVER_STATUS_MAP } from "@/src/types/driver";
|
||||
import type { TableColumn } from "@/src/types/models";
|
||||
import type { Driver } from "@/src/types/driver";
|
||||
|
||||
function fmtDate(iso?: string | null): string {
|
||||
if (!iso) return "—";
|
||||
return new Date(iso).toLocaleDateString("en-US", { 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;
|
||||
}
|
||||
|
||||
interface DriverTableProps {
|
||||
drivers: Driver[];
|
||||
loading: boolean;
|
||||
search: string;
|
||||
page: number;
|
||||
pages: number;
|
||||
onPageChange: (p: number) => void;
|
||||
onRowClick: (driver: Driver) => void;
|
||||
onEdit: (driver: Driver) => void;
|
||||
onDelete: (driver: Driver) => void;
|
||||
}
|
||||
|
||||
export function DriverTable({
|
||||
drivers, loading, search, page, pages,
|
||||
onPageChange, onRowClick, onEdit, onDelete,
|
||||
}: DriverTableProps) {
|
||||
const columns: TableColumn<Driver>[] = [
|
||||
{
|
||||
key: "name",
|
||||
header: "السائقين",
|
||||
width: "2fr",
|
||||
render: (d) => (
|
||||
<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>
|
||||
),
|
||||
},
|
||||
{ key: "branch", header: "الفرع", width: "1.2fr", render: (d) => d.branch?.name ?? "—" },
|
||||
{ key: "nationality", header: "الجنسية", width: "1fr", render: (d) => d.nationality ?? "—" },
|
||||
{
|
||||
key: "licenseExpiry",
|
||||
header: "تاريخ انتهاء الرخصة",
|
||||
width: "1.1fr",
|
||||
render: (d) => {
|
||||
const warn = expirySoon(d.licenseExpiry);
|
||||
return (
|
||||
<span style={{ fontSize: 12, fontWeight: warn ? 600 : 400, color: warn ? "#D97706" : "var(--color-text-secondary)" }}>
|
||||
{warn && "⚠ "}{fmtDate(d.licenseExpiry)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "nationalIdExpiry",
|
||||
header: "تاريخ انتهاء الهوية الوطنية",
|
||||
width: "1.1fr",
|
||||
render: (d) => {
|
||||
const idExpiry = (d as Driver & { nationalIdExpiry?: string }).nationalIdExpiry;
|
||||
const warn = expirySoon(idExpiry);
|
||||
return (
|
||||
<span style={{ fontSize: 12, fontWeight: warn ? 600 : 400, color: warn ? "#D97706" : "var(--color-text-secondary)" }}>
|
||||
{warn && "⚠ "}{fmtDate(idExpiry)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
header: "الحالة",
|
||||
width: "1fr",
|
||||
render: (d) => {
|
||||
const s = DRIVER_STATUS_MAP[d.status] ?? DRIVER_STATUS_MAP.Inactive;
|
||||
return (
|
||||
<span style={{
|
||||
display: "inline-flex", alignItems: "center", gap: 5,
|
||||
borderRadius: "var(--radius-full)",
|
||||
border: `1px solid ${s.border}`,
|
||||
background: s.bg,
|
||||
padding: "0.2rem 0.625rem",
|
||||
fontSize: 11, fontWeight: 600, color: s.color,
|
||||
width: "fit-content",
|
||||
}}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: s.dot, flexShrink: 0 }} />
|
||||
{s.label}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ReusableTable
|
||||
columns={columns}
|
||||
data={drivers}
|
||||
loading={loading}
|
||||
search={search}
|
||||
page={page}
|
||||
pages={pages}
|
||||
onPageChange={onPageChange}
|
||||
actionsWidth="0.8fr"
|
||||
onRowClick={onRowClick}
|
||||
emptyDescription={search ? `No results for "${search}"` : undefined}
|
||||
renderActions={(d) => (
|
||||
<ActionButtons
|
||||
itemLabel={d.name}
|
||||
onEdit={() => onEdit(d)}
|
||||
onDelete={() => onDelete(d)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,142 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import { Button, EmptyState, IconBtn, Spinner } from "../../UI";
|
||||
|
||||
|
||||
import { ReusableTable, ActionButtons } from "../../UI";
|
||||
import { DRIVER_STATUS_MAP } from "@/src/types/driver";
|
||||
import type { TableColumn } from "@/src/types/models";
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
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>
|
||||
const columns: TableColumn<ArchivedDriver>[] = [
|
||||
{
|
||||
key: "name",
|
||||
header: "الاسم",
|
||||
width: "2fr",
|
||||
render: (d) => (
|
||||
<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>
|
||||
) : drivers.length === 0 ? (
|
||||
/* empty state */
|
||||
<EmptyState
|
||||
icon="🗄️"
|
||||
title={search ? `لا توجد نتائج لـ "${search}"` : "لا يوجد سائقون في الأرشيف."}
|
||||
/>
|
||||
) : (
|
||||
/* 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>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "userName",
|
||||
header: "اسم السائق",
|
||||
width: "1.5fr",
|
||||
render: (d) => (
|
||||
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>{d.userName ?? "—"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
header: "الحالة",
|
||||
width: "1fr",
|
||||
render: (d) => {
|
||||
const s = DRIVER_STATUS_MAP[d.status] ?? DRIVER_STATUS_MAP.Inactive;
|
||||
return (
|
||||
<span style={{ display: "inline-flex", alignItems: "center", gap: 5, borderRadius: "var(--radius-full)", border: `1px solid ${s.border}`, background: s.bg, padding: "0.2rem 0.625rem", fontSize: 11, fontWeight: 600, color: s.color }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: s.dot }} />
|
||||
{s.label}
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "createdAt",
|
||||
header: "تاريخ الانشاء",
|
||||
width: "1fr",
|
||||
align: "center",
|
||||
render: (d) => new Date(d.createdAt).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }),
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
header: "تاريخ التعديل",
|
||||
width: "1fr",
|
||||
align: "center",
|
||||
render: (d) => new Date(d.updatedAt).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ReusableTable
|
||||
columns={columns}
|
||||
data={drivers}
|
||||
loading={loading}
|
||||
search={search}
|
||||
page={page}
|
||||
pages={pages}
|
||||
onPageChange={onPageChange}
|
||||
onRowClick={onView}
|
||||
emptyIcon="🗄️"
|
||||
emptyDescription={search ? `No results for "${search}"` : "No archived drivers."}
|
||||
renderActions={(d) => <ActionButtons itemLabel={d.name} onView={() => onView(d)} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -11,18 +11,7 @@ interface ArchivedDriversProps {
|
||||
}
|
||||
|
||||
const searchIcon = (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.35-4.35" />
|
||||
</svg>
|
||||
<i className="ti ti-search" style={{ fontSize: 16 }} aria-hidden="true" />
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user