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:
m7amedez5511
2026-07-26 20:08:12 +03:00
parent b2dcb45606
commit a1a34cb251
70 changed files with 2683 additions and 3243 deletions

View File

@@ -1,18 +1,50 @@
"use client";
import { useCallback, useState } from "react";
import { Spinner, Alert, ArchiveButton, ConfirmDialog, Toast, EmptyState, Button } from "@/src/Components/UI";
import { Spinner, Alert, ArchiveButton, ConfirmDialog } from "@/src/Components/UI";
import { CarFormModal,CarDetailPanel } from "@/src/Components/car";
import { ArchivedCarsModal } from "@/src/Components/car/archive/Archivedcarsmodal";
import { CarMaintenanceFormModal } from "@/src/Components/Car_Maintanance/CarMaintananceFormModal";
import { useCars, useCarMutations, useToast } from "@/src/hooks/useCars";
import { useCarMaintenanceMutations } from "@/src/hooks/UseCarsMaintanance";
import { fmtDateShort, isExpiringSoon, STATUS_MAP, INS_MAP } from "@/src/types/car";
import type { Car, CreateCarPayload, UpdateCarPayload } from "@/src/types/car";
import type { Car, CreateCarPayload, ToastMsg, UpdateCarPayload } from "@/src/types/car";
import type { CreateMaintenancePayload, UpdateMaintenancePayload } from "@/src/types/carMaintanance";
import Header from "@/src/Components/UI/Header";
// ── Toast ─────────────────────────────────────────────────────────────────────
function CarToast({ notification }: { notification: ToastMsg | null }) {
if (!notification) return null;
const ok = notification.type === "success";
return (
<div role="status" aria-live="polite" style={{
position: "fixed", bottom: 24, left: "50%",
transform: "translateX(-50%)",
zIndex: 9999, pointerEvents: "none",
}}>
<div style={{
display: "flex", alignItems: "center", gap: 10,
padding: "0.75rem 1.25rem",
borderRadius: "var(--radius-full)",
background: ok ? "#065F46" : "#7F1D1D",
color: "#FFF", fontSize: 13, fontWeight: 600,
boxShadow: "0 8px 32px rgba(0,0,0,.25)",
maxWidth: "90vw", whiteSpace: "nowrap",
fontFamily: "var(--font-sans)",
}}>
<span style={{ fontSize: 16 }}>{ok ? "✓" : "⚠"}</span>
<span>{notification.message}</span>
</div>
</div>
);
}
// ── CarCard ───────────────────────────────────────────────────────────────────
// NOTE: hover/lift affordance is implemented via Tailwind `hover:`/`focus-visible:`
// classes instead of onMouseEnter/onMouseLeave DOM mutation, so keyboard users
// (tabbing to this card, which is already role="button" tabIndex={0}) get the
// same visual feedback as mouse users via the native :focus-visible pseudo-class.
function CarCard({
car,
@@ -45,23 +77,7 @@ function CarCard({
tabIndex={0}
onKeyDown={e => { if (e.key === "Enter" || e.key === " ") onClick(); }}
aria-label={`${car.manufacturer} ${car.model}${car.plateLetters} ${car.plateNumber}`}
style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
overflow: "hidden",
boxShadow: "var(--shadow-card)",
cursor: "pointer",
transition: "box-shadow 200ms, transform 200ms",
}}
onMouseEnter={e => {
(e.currentTarget as HTMLElement).style.boxShadow = "0 8px 24px rgba(37,99,235,.12)";
(e.currentTarget as HTMLElement).style.transform = "translateY(-2px)";
}}
onMouseLeave={e => {
(e.currentTarget as HTMLElement).style.boxShadow = "var(--shadow-card)";
(e.currentTarget as HTMLElement).style.transform = "translateY(0)";
}}
className="rounded-[var(--radius-xl)] border border-[var(--color-border)] bg-[var(--color-surface)] overflow-hidden shadow-[var(--shadow-card)] cursor-pointer outline-none transition-all duration-200 hover:shadow-[0_8px_24px_rgba(37,99,235,.12)] hover:-translate-y-0.5 focus-visible:shadow-[0_8px_24px_rgba(37,99,235,.12)] focus-visible:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-[var(--color-brand-600)] focus-visible:ring-offset-2"
>
{/* Colour accent bar by status */}
<div style={{
@@ -143,11 +159,7 @@ function CarCard({
</div>
</div>
{/* Send to Maintenance quick action — kept custom (amber accent isn't
one of Button's variants: primary/secondary/ghost/danger). Same
rationale as IconBtn: forcing it into an existing variant would
misrepresent this as a routine action rather than the
maintenance-triggering one it is. */}
{/* Send to Maintenance quick action */}
<button
type="button"
title={maintenanceDisabledReason}
@@ -170,9 +182,7 @@ function CarCard({
fontFamily: "var(--font-sans)",
}}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />
</svg>
<i className="ti ti-tool" style={{ fontSize: 13 }} aria-hidden="true" />
إرسال للصيانة
</button>
@@ -235,7 +245,7 @@ export default function CarsPage() {
// ── Render ──────────────────────────────────────────────────────────────────
return (
<>
<Toast notification={toast} />
<CarToast notification={toast} />
{detailId && (
<CarDetailPanel
@@ -284,7 +294,75 @@ export default function CarsPage() {
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }} dir="rtl">
{/* ── Header ── */}
<Header state={{ total }} search={search} setSearch={setSearch} module="" setModule={() => {}} setPage={setPage} title="المركبات" mainTitle="إدارة المركبات" name="مركبة" isAudit={false} onAdd={() => setFormTarget(null)}/>
<header style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
padding: "1.5rem 2rem",
boxShadow: "var(--shadow-card)",
}}>
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600 }}>
إدارة الأسطول
</p>
<div style={{ marginTop: "0.75rem", display: "flex", flexWrap: "wrap", gap: "1rem", alignItems: "flex-end", justifyContent: "space-between" }}>
<div>
<h1 style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
المركبات
</h1>
<p style={{ marginTop: "0.25rem", fontSize: 13, color: "var(--color-text-muted)" }}>
إجمالي <strong style={{ color: "var(--color-text-primary)" }}>{total}</strong> مركبة في الأسطول
</p>
</div>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}>
{/* Search */}
<div style={{ position: "relative", width: 256 }}>
<i
className="ti ti-search"
aria-hidden="true"
style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", fontSize: 16, color: "var(--color-text-hint)", pointerEvents: "none" }}
/>
<input
type="text"
placeholder="بحث بالماركة أو اللوحة..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
dir="rtl"
style={{
width: "100%", height: 40, paddingRight: 36, paddingLeft: 12,
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13, color: "var(--color-text-primary)",
outline: "none", fontFamily: "var(--font-sans)",
}}
/>
</div>
{/* Add button */}
<button
type="button"
onClick={() => setFormTarget(null)}
style={{
height: 40, padding: "0 1.125rem",
borderRadius: "var(--radius-lg)",
border: "none",
background: "var(--color-brand-600)",
fontSize: 13, fontWeight: 700, color: "#FFF",
cursor: "pointer",
display: "inline-flex", alignItems: "center", gap: 7,
fontFamily: "var(--font-sans)",
boxShadow: "0 1px 4px rgba(37,99,235,.35)",
whiteSpace: "nowrap",
}}
>
<i className="ti ti-plus" style={{ fontSize: 14 }} aria-hidden="true" />
إضافة مركبة
</button>
</div>
</div>
</header>
{/* Error alert */}
{error && <Alert type="error" message={error} onClose={() => setError(null)} />}
@@ -295,16 +373,36 @@ export default function CarsPage() {
<span style={{ fontSize: 14 }}>جارٍ تحميل المركبات</span>
</div>
) : cars.length === 0 ? (
<EmptyState
icon="🚗"
title={search ? `لا توجد مركبات تطابق "${search}"` : "لا توجد مركبات بعد"}
description='اضغط على "إضافة مركبة" لإضافة أول مركبة في الأسطول.'
action={!search && (
<Button variant="primary" onClick={() => setFormTarget(null)}>
<div style={{
borderRadius: "var(--radius-xl)",
border: "2px dashed var(--color-border)",
background: "var(--color-surface)",
padding: "5rem 2rem",
textAlign: "center",
}}>
<div style={{ fontSize: 52, marginBottom: 16 }}>🚗</div>
<p style={{ fontSize: 16, fontWeight: 600, color: "var(--color-text-primary)" }}>
{search ? `لا توجد مركبات تطابق "${search}"` : "لا توجد مركبات بعد"}
</p>
<p style={{ fontSize: 13, color: "var(--color-text-muted)", marginTop: 6 }}>
اضغط على &quot;إضافة مركبة&quot; لإضافة أول مركبة في الأسطول.
</p>
{!search && (
<button
type="button"
onClick={() => setFormTarget(null)}
style={{
marginTop: 16, height: 40, padding: "0 1.5rem",
borderRadius: "var(--radius-lg)",
border: "none", background: "var(--color-brand-600)",
fontSize: 13, fontWeight: 700, color: "#FFF",
cursor: "pointer", fontFamily: "var(--font-sans)",
}}
>
إضافة مركبة
</Button>
</button>
)}
/>
</div>
) : (
/* ── Card grid ── */
<div style={{
@@ -312,6 +410,14 @@ export default function CarsPage() {
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
gap: "1rem",
}}>
{/* PERF NOTE: Cards are rendered inline via .map() below. At the
current pagination size (10-12 items) this is not a bottleneck.
If page size increases (e.g. "show all" mode, a larger
page-size selector, or removal of server-side pagination),
extract <CarCard> is already its own component above — wrap
it in React.memo at that point, and make sure onClick /
onSendToMaintenance passed to it are stable (useCallback)
so memoization is actually effective. */}
{cars.map((car) => (
<CarCard
key={car.id}
@@ -344,15 +450,24 @@ export default function CarsPage() {
{ label: "← السابق", action: () => setPage(p => Math.max(1, p - 1)), disabled: page === 1 },
{ label: "التالي →", action: () => setPage(p => Math.min(pages, p + 1)), disabled: page === pages },
].map((btn) => (
<Button
<button
key={btn.label}
variant="secondary"
size="sm"
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)",
fontWeight: 600,
}}
>
{btn.label}
</Button>
</button>
))}
</div>
</div>