first action update user pages .
2-update driver pages [fixed image display and notyfi] 3-update car pages [fixed image] 4-build tripe pages crud 5-build some role pages 6-build audit page 7-update ui compounants 8-update saidebar and topbar 9-cheange view to be ar view 10-cheange stractcher to be app,src 11-add validation layer by yup 12-add api image-proxy to broke image corse bloken
This commit is contained in:
@@ -1,15 +1,12 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { Alert, Spinner } from "@/Components/UI";
|
||||
import { DriverFormModal } from "@/Components/Driver/DriverFormModal";
|
||||
import { DriverDeleteModal } from "@/Components/Driver/DriverDeleteModal";
|
||||
import { driverService } from "@/services";
|
||||
import { getStoredToken } from "@/lib/auth";
|
||||
import { useDrivers } from "@/hooks/useDriver";
|
||||
import { CreateDriverPayload, Driver, DRIVER_STATUS_MAP, UpdateDriverPayload } from "@/types/driver";
|
||||
import { DriverDetailPanel } from "@/Components/Driver/DriverDetailPanel";
|
||||
import { Alert, Spinner } from "@/src/Components/UI";
|
||||
import { DriverFormModal } from "@/src/Components/Driver/DriverFormModal";
|
||||
import { DriverDeleteModal } from "@/src/Components/Driver/DriverDeleteModal";
|
||||
import { useDrivers } from "@/src/hooks/useDriver";
|
||||
import { CreateDriverPayload, Driver, DRIVER_STATUS_MAP, UpdateDriverPayload } from "@/src/types/driver";
|
||||
import { DriverDetailPanel } from "@/src/Components/Driver/DriverDetailPanel";
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -46,13 +43,28 @@ const thStyle: React.CSSProperties = {
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
};
|
||||
|
||||
// Shared across header + rows — keep these in sync or columns will misalign.
|
||||
const ROW_GRID_COLUMNS = "2fr 1.2fr 1fr 1.1fr 1.1fr 1fr 0.8fr";
|
||||
|
||||
const iconBtnBase: React.CSSProperties = {
|
||||
width: 30,
|
||||
height: 30,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "var(--radius-md)",
|
||||
cursor: "pointer",
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
// ── Page Component ────────────────────────────────────────────────────────────
|
||||
|
||||
export default function DriversPage() {
|
||||
const {
|
||||
drivers, loading, error, total, pages, page,
|
||||
search, setPage, handleSearch, clearError,
|
||||
deleteDriver, notification, reload,
|
||||
createDriver, updateDriver, deleteDriver,
|
||||
notification, reload,
|
||||
} = useDrivers();
|
||||
|
||||
// ── Panel / modal state ───────────────────────────────────────────────────
|
||||
@@ -60,6 +72,8 @@ export default function DriversPage() {
|
||||
const [formDriver, setFormDriver] = useState<Driver | null | "new">(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Driver | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
// Bumped after a successful edit to force the detail panel to re-fetch
|
||||
const [panelRefreshKey, setPanelRefreshKey] = useState(0);
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────────
|
||||
const handleEdit = useCallback((driver: Driver) => {
|
||||
@@ -77,34 +91,33 @@ export default function DriversPage() {
|
||||
payload: CreateDriverPayload | UpdateDriverPayload,
|
||||
isNew: boolean,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
if (isNew) {
|
||||
// Use multipart if there are file fields
|
||||
const hasFiles =
|
||||
(payload as CreateDriverPayload & { photo?: File }).photo ||
|
||||
(payload as CreateDriverPayload & { nationalPhoto?: File }).nationalPhoto ||
|
||||
(payload as CreateDriverPayload & { driverCardPhoto?: File }).driverCardPhoto;
|
||||
let ok: boolean;
|
||||
|
||||
if (hasFiles) {
|
||||
await driverService.createWithImages(
|
||||
payload as Parameters<typeof driverService.createWithImages>[0],
|
||||
token,
|
||||
);
|
||||
} else {
|
||||
await driverService.create(payload as CreateDriverPayload, token);
|
||||
}
|
||||
} else {
|
||||
const id = (formDriver as Driver).id;
|
||||
await driverService.update(id, payload as UpdateDriverPayload, token);
|
||||
}
|
||||
reload();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
if (isNew) {
|
||||
ok = await createDriver(
|
||||
payload as CreateDriverPayload & {
|
||||
photo?: File;
|
||||
nationalPhoto?: File;
|
||||
driverCardPhoto?: File;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const id = (formDriver as Driver).id;
|
||||
ok = await updateDriver(
|
||||
id,
|
||||
payload as UpdateDriverPayload & {
|
||||
photo?: File;
|
||||
nationalPhoto?: File;
|
||||
driverCardPhoto?: File;
|
||||
},
|
||||
);
|
||||
// Re-fetch detail panel so updated status is visible immediately
|
||||
if (ok && selectedDriverId) setPanelRefreshKey((k) => k + 1);
|
||||
}
|
||||
|
||||
return ok;
|
||||
},
|
||||
[formDriver, reload],
|
||||
[formDriver, createDriver, updateDriver, selectedDriverId],
|
||||
);
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
@@ -198,10 +211,7 @@ export default function DriversPage() {
|
||||
|
||||
{/* ── Notifications ── */}
|
||||
{notification && (
|
||||
<Alert
|
||||
type={notification.type}
|
||||
message={notification.message}
|
||||
/>
|
||||
<Alert type={notification.type} message={notification.message} />
|
||||
)}
|
||||
{error && (
|
||||
<Alert type="error" message={error} onClose={clearError} />
|
||||
@@ -213,7 +223,7 @@ export default function DriversPage() {
|
||||
dir="rtl"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "2fr 1.2fr 1fr 1.1fr 1.1fr 1fr",
|
||||
gridTemplateColumns: ROW_GRID_COLUMNS,
|
||||
...thStyle,
|
||||
}}
|
||||
>
|
||||
@@ -223,6 +233,7 @@ export default function DriversPage() {
|
||||
<span>انتهاء الرخصة</span>
|
||||
<span>انتهاء الهوية</span>
|
||||
<span>الحالة</span>
|
||||
<span>إجراءات</span>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
@@ -247,7 +258,7 @@ export default function DriversPage() {
|
||||
onClick={() => setSelectedDriverId(d.id)}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "2fr 1.2fr 1fr 1.1fr 1.1fr 1fr",
|
||||
gridTemplateColumns: ROW_GRID_COLUMNS,
|
||||
alignItems: "center", gap: "0.5rem",
|
||||
padding: "0.875rem 1.5rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
@@ -259,29 +270,18 @@ export default function DriversPage() {
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = "var(--color-surface-hover, #F8FAFC)")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent")}
|
||||
>
|
||||
{/* Name + phone */}
|
||||
<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>
|
||||
|
||||
{/* Branch */}
|
||||
<span style={{ color: "var(--color-text-secondary)" }}>{d.branch?.name ?? "—"}</span>
|
||||
|
||||
{/* Nationality */}
|
||||
<span style={{ color: "var(--color-text-secondary)" }}>{d.nationality ?? "—"}</span>
|
||||
|
||||
{/* License expiry */}
|
||||
<span style={{ fontSize: 12, fontWeight: licWarn ? 600 : 400, color: licWarn ? "#D97706" : "var(--color-text-secondary)" }}>
|
||||
{licWarn && "⚠ "}{fmtDate(d.licenseExpiry)}
|
||||
</span>
|
||||
|
||||
{/* National ID expiry */}
|
||||
<span style={{ fontSize: 12, fontWeight: idWarn ? 600 : 400, color: idWarn ? "#D97706" : "var(--color-text-secondary)" }}>
|
||||
{idWarn && "⚠ "}{fmtDate((d as Driver & { nationalIdExpiry?: string }).nationalIdExpiry)}
|
||||
</span>
|
||||
|
||||
{/* Status badge */}
|
||||
<span style={{
|
||||
display: "inline-flex", alignItems: "center", gap: 5,
|
||||
borderRadius: "var(--radius-full)",
|
||||
@@ -294,6 +294,50 @@ export default function DriversPage() {
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusCfg.dot, flexShrink: 0 }} />
|
||||
{statusCfg.label}
|
||||
</span>
|
||||
|
||||
{/* ── Inline row actions: edit / delete ──
|
||||
stopPropagation is required on both buttons so the
|
||||
click doesn't bubble up to the <li> onClick and
|
||||
open the detail panel as well. */}
|
||||
<div style={{ display: "flex", gap: "0.4rem" }}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="تعديل السائق"
|
||||
title="تعديل"
|
||||
onClick={(e) => { e.stopPropagation(); handleEdit(d); }}
|
||||
style={{
|
||||
...iconBtnBase,
|
||||
border: "1px solid var(--color-brand-200)",
|
||||
background: "var(--color-brand-50, #EFF6FF)",
|
||||
color: "var(--color-brand-600)",
|
||||
}}
|
||||
>
|
||||
<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
|
||||
type="button"
|
||||
aria-label="حذف السائق"
|
||||
title="حذف"
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(d); }}
|
||||
style={{
|
||||
...iconBtnBase,
|
||||
border: "1px solid #FECACA",
|
||||
background: "#FEF2F2",
|
||||
color: "#DC2626",
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="3 6 5 6 21 6" />
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||
<path d="M10 11v6M14 11v6" />
|
||||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -344,9 +388,10 @@ export default function DriversPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Detail panel ── */}
|
||||
{/* ── Detail panel — key forces re-fetch after successful edit ── */}
|
||||
{selectedDriverId && (
|
||||
<DriverDetailPanel
|
||||
key={`${selectedDriverId}-${panelRefreshKey}`}
|
||||
driverId={selectedDriverId}
|
||||
onClose={() => setSelectedDriverId(null)}
|
||||
onEdit={handleEdit}
|
||||
|
||||
Reference in New Issue
Block a user