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,19 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { Spinner } from "@/Components/UI";
|
||||
import { TripFormModal } from "@/Components/Trip/Tripformmodal";
|
||||
import { TripDeleteModal } from "@/Components/Trip/Tripdeletemodal";
|
||||
import { TripReportPanel } from "@/Components/Trip_Report/Tripreportpanel";
|
||||
import { tripService } from "@/services/trip.service";
|
||||
import { getStoredToken } from "@/lib/auth";
|
||||
import type {
|
||||
Trip,
|
||||
CreateTripPayload,
|
||||
UpdateTripPayload,
|
||||
} from "@/types/trip";
|
||||
import { TRIP_STATUS_MAP } from "@/types/trip";
|
||||
import { Spinner } from "@/src/Components/UI";
|
||||
import { TripFormModal } from "@/src/Components/Trip/Tripformmodal";
|
||||
import { TripDeleteModal } from "@/src/Components/Trip/Tripdeletemodal";
|
||||
import { TripReportPanel } from "@/src/Components/Trip_Report/Tripreportpanel";
|
||||
import { tripService } from "@/src/services/trip.service";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
import type { Trip, CreateTripPayload, UpdateTripPayload } from "@/src/types/trip";
|
||||
import { TRIP_STATUS_MAP } from "@/src/types/trip";
|
||||
import { Toast, type ToastNotification } from "@/src/Components/UI/Toast";
|
||||
|
||||
// ── API error extractor ────────────────────────────────────────────────────
|
||||
// Same shape-walking logic as useTrip.ts — duplicated here on purpose so this
|
||||
// page no longer depends on the list hook for a single piece of UI feedback.
|
||||
|
||||
function extractApiMessage(err: unknown, fallback: string): string {
|
||||
if (typeof err === "string" && err.trim()) return err.trim();
|
||||
|
||||
if (err && typeof err === "object") {
|
||||
const e = err as Record<string, unknown>;
|
||||
const responseData = (e["response"] as Record<string, unknown> | undefined)?.["data"];
|
||||
if (responseData && typeof responseData === "object") {
|
||||
const rd = responseData as Record<string, unknown>;
|
||||
if (typeof rd["message"] === "string" && rd["message"].trim()) return rd["message"];
|
||||
if (Array.isArray(rd["message"])) return (rd["message"] as string[]).join(" — ");
|
||||
if (typeof rd["error"] === "string" && rd["error"].trim()) return rd["error"];
|
||||
}
|
||||
if (typeof e["message"] === "string" && e["message"].trim()) return e["message"];
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -23,19 +42,26 @@ function fmtDate(iso?: string | null): string {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function fmtNumber(n?: number | string | null): string {
|
||||
if (n == null || n === "") return "—";
|
||||
return Number(n).toLocaleString("ar-SA");
|
||||
function fmtDateTime(iso?: string | null): string {
|
||||
if (!iso) return "—";
|
||||
return new Date(iso).toLocaleString("ar-SA", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sub-components ────────────────────────────────────────────────────────────
|
||||
|
||||
function SectionCard({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
function SectionCard({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -46,19 +72,23 @@ function SectionCard({ title, children }: { title: string; children: React.React
|
||||
boxShadow: "var(--shadow-card)",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
padding: "0.875rem 1.5rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface-muted)",
|
||||
}}>
|
||||
<p style={{
|
||||
fontSize: 11,
|
||||
letterSpacing: "0.25em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--color-text-hint)",
|
||||
fontWeight: 700,
|
||||
margin: 0,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
padding: "0.875rem 1.5rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface-muted)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 11,
|
||||
letterSpacing: "0.25em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--color-text-hint)",
|
||||
fontWeight: 700,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
@@ -71,63 +101,70 @@ function DetailRow({
|
||||
label,
|
||||
value,
|
||||
mono = false,
|
||||
warn = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
mono?: boolean;
|
||||
warn?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
padding: "0.55rem 0",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
padding: "0.55rem 0",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12, color: "var(--color-text-muted)", fontWeight: 600 }}>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontFamily: mono ? "var(--font-mono)" : "var(--font-sans)",
|
||||
color: "var(--color-text-primary)",
|
||||
maxWidth: "60%",
|
||||
textAlign: "left",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontFamily: mono ? "var(--font-mono)" : "var(--font-sans)",
|
||||
color: warn ? "#D97706" : "var(--color-text-primary)",
|
||||
fontWeight: warn ? 600 : 400,
|
||||
maxWidth: "60%",
|
||||
textAlign: "left",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{warn && value !== "—" ? "⚠ " : ""}
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Stat card (للأعداد) ───────────────────────────────────────────────────────
|
||||
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
color = "var(--color-brand-600)",
|
||||
bg = "var(--color-brand-50, #EFF6FF)",
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
color?: string;
|
||||
bg?: string;
|
||||
value: number | string;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
borderRadius: "var(--radius-lg)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: bg,
|
||||
padding: "1rem 1.25rem",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
}}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: "var(--color-text-muted)" }}>
|
||||
<div
|
||||
style={{
|
||||
padding: "1rem 1.25rem",
|
||||
background: "var(--color-surface)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: "var(--radius-lg)",
|
||||
boxShadow: "var(--shadow-card)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, color: "var(--color-text-muted)", letterSpacing: "0.05em" }}>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{ fontSize: 22, fontWeight: 700, color, fontFamily: "var(--font-mono)" }}>
|
||||
<span style={{ fontSize: 22, fontWeight: 800, color, fontFamily: "var(--font-mono)" }}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
@@ -141,14 +178,31 @@ export default function TripDetailPage() {
|
||||
const router = useRouter();
|
||||
const tripId = params?.tripId as string;
|
||||
|
||||
const [trip, setTrip] = useState<Trip | null>(null);
|
||||
const [trip, setTrip] = useState<Trip | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// ── Modal state ───────────────────────────────────────────────────────────
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// ── Notifications (standalone — no longer borrowed from the list hook) ───
|
||||
const [notification, setNotification] = useState<ToastNotification | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const notify = useCallback((n: ToastNotification) => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
setNotification(n);
|
||||
timerRef.current = setTimeout(() => setNotification(null), 4000);
|
||||
}, []);
|
||||
|
||||
const dismissNotification = useCallback(() => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
setNotification(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
|
||||
|
||||
// ── Load trip ─────────────────────────────────────────────────────────────
|
||||
const loadTrip = useCallback(async () => {
|
||||
@@ -166,7 +220,9 @@ export default function TripDetailPage() {
|
||||
}
|
||||
}, [tripId]);
|
||||
|
||||
useEffect(() => { loadTrip(); }, [loadTrip]);
|
||||
useEffect(() => {
|
||||
loadTrip();
|
||||
}, [loadTrip]);
|
||||
|
||||
// ── Edit submit ───────────────────────────────────────────────────────────
|
||||
const handleEditSubmit = useCallback(
|
||||
@@ -177,299 +233,317 @@ export default function TripDetailPage() {
|
||||
if (!trip) return false;
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
await tripService.update(trip.id, payload as UpdateTripPayload, token);
|
||||
await loadTrip();
|
||||
const res = await tripService.update(trip.id, payload as UpdateTripPayload, token);
|
||||
const updated = (res as unknown as { data: Trip }).data;
|
||||
setTrip(updated);
|
||||
notify({ type: "success", message: "تم تحديث بيانات الرحلة بنجاح." });
|
||||
return true;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
notify({ type: "error", message: extractApiMessage(err, "تعذّر تحديث الرحلة.") });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[trip, loadTrip],
|
||||
[trip, notify],
|
||||
);
|
||||
|
||||
// ── Delete ────────────────────────────────────────────────────────────────
|
||||
// ── Delete confirm ────────────────────────────────────────────────────────
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (!trip) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
await tripService.delete(trip.id, token);
|
||||
router.back();
|
||||
} catch {
|
||||
router.push("/dashboard/trips");
|
||||
} catch (err) {
|
||||
notify({ type: "error", message: extractApiMessage(err, "تعذّر حذف الرحلة.") });
|
||||
setDeleting(false);
|
||||
setDeleteOpen(false);
|
||||
}
|
||||
}, [trip, router]);
|
||||
}, [trip, router, notify]);
|
||||
|
||||
// ── Status config ─────────────────────────────────────────────────────────
|
||||
const statusConfig = trip ? TRIP_STATUS_MAP[trip.status] : null;
|
||||
|
||||
// ── Loading ───────────────────────────────────────────────────────────────
|
||||
// ── Render: Loading ───────────────────────────────────────────────────────
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: "flex", justifyContent: "center", padding: "4rem" }}>
|
||||
<Spinner size="lg" />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 12,
|
||||
padding: "6rem 0",
|
||||
color: "var(--color-text-muted)",
|
||||
}}
|
||||
>
|
||||
<Spinner size="sm" className="text-blue-600" />
|
||||
<span style={{ fontSize: 14 }}>جارٍ التحميل…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Error ─────────────────────────────────────────────────────────────────
|
||||
// ── Render: Error ─────────────────────────────────────────────────────────
|
||||
if (error || !trip) {
|
||||
return (
|
||||
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||||
<p style={{ fontSize: 14, color: "var(--color-danger)", marginBottom: "1rem" }}>
|
||||
{error ?? "الرحلة غير موجودة."}
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 480,
|
||||
margin: "4rem auto",
|
||||
borderRadius: "var(--radius-xl)",
|
||||
border: "1px solid #FECACA",
|
||||
background: "#FEF2F2",
|
||||
padding: "1.5rem",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<p style={{ fontSize: 14, color: "#DC2626", fontWeight: 600 }}>
|
||||
⚠ {error ?? "الرحلة غير موجودة"}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
style={{
|
||||
height: 38, padding: "0 1.25rem",
|
||||
marginTop: "1rem",
|
||||
height: 38,
|
||||
padding: "0 1.25rem",
|
||||
borderRadius: "var(--radius-md)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
fontSize: 13, fontWeight: 600, cursor: "pointer",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "var(--color-text-secondary)",
|
||||
cursor: "pointer",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
← العودة
|
||||
← رجوع
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Render: Trip detail ────────────────────────────────────────────────────
|
||||
return (
|
||||
<>
|
||||
<section style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
||||
{/* ── Toast notification ── */}
|
||||
<Toast notification={notification} onDismiss={dismissNotification} />
|
||||
|
||||
{/* ── Header ── */}
|
||||
<header style={{
|
||||
borderRadius: "var(--radius-xl)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
boxShadow: "var(--shadow-card)",
|
||||
padding: "1.5rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
flexWrap: "wrap",
|
||||
gap: "1rem",
|
||||
}}>
|
||||
{/* Back + trip info */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
style={{
|
||||
width: 38, height: 38, borderRadius: "var(--radius-md)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface-muted)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
cursor: "pointer", flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
</button>
|
||||
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
||||
|
||||
{/* Trip icon */}
|
||||
<div style={{
|
||||
width: 52, height: 52, borderRadius: "var(--radius-lg)",
|
||||
background: "var(--color-brand-50, #EFF6FF)",
|
||||
border: "1px solid var(--color-brand-200, #BFDBFE)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
|
||||
}}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2563EB" strokeWidth="1.8">
|
||||
<rect x="1" y="3" width="15" height="13" rx="2" />
|
||||
<path d="M16 8h4l3 5v4h-7V8z" />
|
||||
<circle cx="5.5" cy="18.5" r="2.5" />
|
||||
<circle cx="18.5" cy="18.5" r="2.5" />
|
||||
</svg>
|
||||
</div>
|
||||
{/* ── Page header ── */}
|
||||
<header
|
||||
style={{
|
||||
borderRadius: "var(--radius-xl)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
padding: "1.5rem 2rem",
|
||||
boxShadow: "var(--shadow-card)",
|
||||
}}
|
||||
>
|
||||
{/* Back link */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: "var(--color-text-muted)",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: 0,
|
||||
marginBottom: "1rem",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M19 12H5M12 5l-7 7 7 7" />
|
||||
</svg>
|
||||
العودة إلى قائمة الرحلات
|
||||
</button>
|
||||
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<h1 style={{ fontSize: 18, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
|
||||
{trip.title}
|
||||
</h1>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
||||
<span style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--color-text-muted)" }}>
|
||||
{trip.tripNumber}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: "1rem" }}>
|
||||
{/* Icon + title */}
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: "50%",
|
||||
overflow: "hidden",
|
||||
flexShrink: 0,
|
||||
border: "2px solid var(--color-brand-200)",
|
||||
background: "var(--color-surface-muted)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 28, fontWeight: 700, color: "var(--color-brand-600)" }}>
|
||||
{trip.title.charAt(0)}
|
||||
</span>
|
||||
{statusConfig && (
|
||||
<span style={{
|
||||
borderRadius: "var(--radius-full)",
|
||||
border: `1px solid ${statusConfig.border}`,
|
||||
background: statusConfig.bg,
|
||||
padding: "0.2rem 0.625rem",
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: statusConfig.color,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
}}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusConfig.dot, flexShrink: 0 }} />
|
||||
{statusConfig.label}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600, margin: 0 }}>
|
||||
ملف الرحلة
|
||||
</p>
|
||||
<h1 style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||
{trip.title}
|
||||
</h1>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 6, flexWrap: "wrap" }}>
|
||||
<span style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--color-text-muted)" }}>
|
||||
#{trip.tripNumber}
|
||||
</span>
|
||||
)}
|
||||
{trip.isDeleted && (
|
||||
<span style={{
|
||||
borderRadius: "var(--radius-full)",
|
||||
border: "1px solid #FECACA",
|
||||
background: "#FEF2F2",
|
||||
padding: "0.2rem 0.625rem",
|
||||
fontSize: 11, fontWeight: 700, color: "#DC2626",
|
||||
}}>
|
||||
محذوفة
|
||||
</span>
|
||||
)}
|
||||
{statusConfig && (
|
||||
<span
|
||||
style={{
|
||||
borderRadius: "var(--radius-full)",
|
||||
border: `1px solid ${statusConfig.border}`,
|
||||
background: statusConfig.bg,
|
||||
padding: "0.2rem 0.625rem",
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: statusConfig.color,
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusConfig.dot, flexShrink: 0 }} />
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: "flex", gap: "0.75rem" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
style={{
|
||||
height: 40, padding: "0 1.25rem",
|
||||
borderRadius: "var(--radius-lg)",
|
||||
border: "1px solid #FECACA",
|
||||
background: "#FEF2F2",
|
||||
fontSize: 13, fontWeight: 700, color: "#DC2626",
|
||||
cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
حذف الرحلة
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditOpen(true)}
|
||||
style={{
|
||||
height: 40, padding: "0 1.25rem",
|
||||
borderRadius: "var(--radius-lg)",
|
||||
border: "none",
|
||||
background: "var(--color-brand-600)",
|
||||
fontSize: 13, fontWeight: 700, color: "#FFF",
|
||||
cursor: "pointer",
|
||||
display: "flex", alignItems: "center", gap: 8,
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{/* Actions */}
|
||||
<div style={{ display: "flex", gap: "0.75rem" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
style={{
|
||||
height: 40,
|
||||
padding: "0 1.25rem",
|
||||
borderRadius: "var(--radius-lg)",
|
||||
border: "1px solid #FECACA",
|
||||
background: "#FEF2F2",
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
color: "#DC2626",
|
||||
cursor: "pointer",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
حذف الرحلة
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditOpen(true)}
|
||||
style={{
|
||||
height: 40,
|
||||
padding: "0 1.25rem",
|
||||
borderRadius: "var(--radius-lg)",
|
||||
border: "none",
|
||||
background: "var(--color-brand-600)",
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
color: "#FFF",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ── Stats row ── */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "1rem" }}>
|
||||
<StatCard
|
||||
label="المجمّع"
|
||||
value={String(trip.collectedCount)}
|
||||
color="#1E40AF"
|
||||
bg="#EFF6FF"
|
||||
/>
|
||||
<StatCard
|
||||
label="المُسلَّم"
|
||||
value={String(trip.deliveredCount)}
|
||||
color="#166534"
|
||||
bg="#DCFCE7"
|
||||
/>
|
||||
<StatCard
|
||||
label="المُرتجع"
|
||||
value={String(trip.returnedCount)}
|
||||
color="#92400E"
|
||||
bg="#FFFBEB"
|
||||
/>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(150px, 1fr))", gap: "0.75rem" }}>
|
||||
<StatCard label="المجمّع" value={trip.collectedCount} color="var(--color-brand-600)" />
|
||||
<StatCard label="المُسلَّم" value={trip.deliveredCount} color="#16A34A" />
|
||||
<StatCard label="المُرتجع" value={trip.returnedCount} color="#D97706" />
|
||||
<StatCard
|
||||
label="النقد المحصّل"
|
||||
value={fmtNumber(trip.totalCashCollected)}
|
||||
color="#581C87"
|
||||
bg="#FAF5FF"
|
||||
value={trip.totalCashCollected != null ? `${Number(trip.totalCashCollected).toLocaleString("ar-SA")} ر.س` : "—"}
|
||||
color="#7C3AED"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Content grid ── */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1.5rem" }}>
|
||||
|
||||
{/* Basic Info */}
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: "1.5rem",
|
||||
}}
|
||||
>
|
||||
{/* Trip Info */}
|
||||
<SectionCard title="بيانات الرحلة">
|
||||
<DetailRow label="رقم الرحلة" value={trip.tripNumber} mono />
|
||||
<DetailRow label="العنوان" value={trip.title} />
|
||||
<DetailRow label="الحالة" value={statusConfig?.label ?? trip.status} />
|
||||
<DetailRow label="وقت البدء" value={fmtDateTime(trip.startTime)} />
|
||||
<DetailRow label="وقت الانتهاء" value={fmtDateTime(trip.endTime)} />
|
||||
<DetailRow label="الفرع" value={trip.branch?.name ?? "—"} />
|
||||
<DetailRow label="وقت البدء" value={fmtDate(trip.startTime)} />
|
||||
<DetailRow label="وقت الانتهاء" value={fmtDate(trip.endTime)} />
|
||||
<DetailRow label="سبب الإنهاء" value={trip.endReason ?? "—"} />
|
||||
{trip.notes && <DetailRow label="ملاحظات" value={trip.notes} />}
|
||||
{trip.endReason && <DetailRow label="سبب الإنهاء" value={trip.endReason} warn />}
|
||||
</SectionCard>
|
||||
|
||||
{/* Driver */}
|
||||
<SectionCard title="السائق">
|
||||
{trip.driver ? (
|
||||
<>
|
||||
<DetailRow label="الاسم" value={trip.driver.name} />
|
||||
<DetailRow label="الجوال" value={trip.driver.phone} mono />
|
||||
<DetailRow label="البريد" value={trip.driver.email ?? "—"} />
|
||||
<DetailRow label="رقم الرخصة" value={trip.driver.licenseNumber ?? "—"} mono />
|
||||
<DetailRow label="رقم البطاقة" value={trip.driver.driverCardNumber ?? "—"} mono />
|
||||
<DetailRow label="الجنسية" value={trip.driver.nationality ?? "—"} />
|
||||
</>
|
||||
) : (
|
||||
<p style={{ fontSize: 13, color: "var(--color-text-muted)", margin: 0 }}>لا توجد بيانات للسائق.</p>
|
||||
)}
|
||||
</SectionCard>
|
||||
{trip.driver && (
|
||||
<SectionCard title="بيانات السائق">
|
||||
<DetailRow label="الاسم" value={trip.driver.name} />
|
||||
<DetailRow label="الجوال" value={trip.driver.phone} mono />
|
||||
<DetailRow label="اسم المستخدم" value={trip.driver.userName ?? "—"} mono />
|
||||
<DetailRow label="البريد" value={trip.driver.email ?? "—"} />
|
||||
<DetailRow label="الجنسية" value={trip.driver.nationality ?? "—"} />
|
||||
<DetailRow label="رخصة القيادة" value={trip.driver.licenseNumber ?? "—"} mono />
|
||||
<DetailRow label="بطاقة السائق" value={trip.driver.driverCardNumber ?? "—"} mono />
|
||||
<DetailRow label="رقم GOSI" value={trip.driver.gosiNumber ?? "—"} mono />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* Car */}
|
||||
<SectionCard title="السيارة">
|
||||
{trip.car ? (
|
||||
<>
|
||||
<DetailRow label="الماركة" value={trip.car.manufacturer} />
|
||||
<DetailRow label="الموديل" value={trip.car.model} />
|
||||
<DetailRow label="السنة" value={trip.car.year != null ? String(trip.car.year) : "—"} />
|
||||
<DetailRow label="اللون" value={trip.car.color ?? "—"} />
|
||||
<DetailRow label="رقم اللوحة" value={trip.car.plateNumber} mono />
|
||||
<DetailRow label="حروف اللوحة" value={trip.car.plateLetters ?? "—"} mono />
|
||||
<DetailRow label="نوع اللوحة" value={trip.car.plateType ?? "—"} />
|
||||
<DetailRow label="رقم التسجيل" value={trip.car.registrationNumber ?? "—"} mono />
|
||||
</>
|
||||
) : (
|
||||
<p style={{ fontSize: 13, color: "var(--color-text-muted)", margin: 0 }}>لا توجد بيانات للسيارة.</p>
|
||||
)}
|
||||
</SectionCard>
|
||||
{trip.car && (
|
||||
<SectionCard title="بيانات السيارة">
|
||||
<DetailRow label="الشركة" value={trip.car.manufacturer} />
|
||||
<DetailRow label="الموديل" value={trip.car.model} />
|
||||
<DetailRow label="السنة" value={trip.car.year != null ? String(trip.car.year) : "—"} />
|
||||
<DetailRow label="اللون" value={trip.car.color ?? "—"} />
|
||||
<DetailRow label="رقم اللوحة" value={trip.car.plateNumber} mono />
|
||||
<DetailRow label="حروف اللوحة" value={trip.car.plateLetters ?? "—"} mono />
|
||||
<DetailRow label="رقم التسجيل" value={trip.car.registrationNumber ?? "—"} mono />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* System Info */}
|
||||
<SectionCard title="معلومات النظام">
|
||||
<DetailRow label="تاريخ الإضافة" value={fmtDate(trip.createdAt)} />
|
||||
<DetailRow label="رقم الرحلة" value={trip.tripNumber} mono />
|
||||
<DetailRow label="تاريخ الإنشاء" value={fmtDate(trip.createdAt)} />
|
||||
<DetailRow label="آخر تحديث" value={fmtDate(trip.updatedAt)} />
|
||||
{trip.isDeleted && (
|
||||
<DetailRow label="تاريخ الحذف" value={fmtDate(trip.deletedAt)} />
|
||||
)}
|
||||
</SectionCard>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ── Notes (full width, if any) ── */}
|
||||
{trip.notes && (
|
||||
<SectionCard title="الملاحظات">
|
||||
<p style={{ fontSize: 13, color: "var(--color-text-primary)", lineHeight: 1.7, margin: 0 }}>
|
||||
{trip.notes}
|
||||
</p>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* ── Report section ── */}
|
||||
<div style={{
|
||||
borderRadius: "var(--radius-xl)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
boxShadow: "var(--shadow-card)",
|
||||
padding: "1.5rem",
|
||||
}}>
|
||||
{/* ── Report section (full width) ── */}
|
||||
<div
|
||||
style={{
|
||||
borderRadius: "var(--radius-xl)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
boxShadow: "var(--shadow-card)",
|
||||
padding: "1.5rem",
|
||||
}}
|
||||
>
|
||||
<TripReportPanel tripId={trip.id} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,375 @@
|
||||
export default function TripsPage() {
|
||||
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Trips module is ready for scheduling, status tracking, and manifest generation.</section>;
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTrips } from "@/src/hooks/useTrip";
|
||||
import { tripService } from "@/src/services/trip.service";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
import { TripFormModal } from "@/src/Components/Trip/Tripformmodal";
|
||||
import { TripDeleteModal } from "@/src/Components/Trip/Tripdeletemodal";
|
||||
import { Spinner } from "@/src/Components/UI";
|
||||
import { Toast, type ToastNotification } from "@/src/Components/UI/Toast";
|
||||
import type { Trip, TripStatus, CreateTripPayload, UpdateTripPayload } from "@/src/types/trip";
|
||||
import { TRIP_STATUS_MAP } from "@/src/types/trip";
|
||||
|
||||
// ── API error extractor ────────────────────────────────────────────────────
|
||||
// Same logic as the detail page / useTrip.ts — kept local so create/update
|
||||
// here can notify independently of the list hook's own notification state.
|
||||
|
||||
function extractApiMessage(err: unknown, fallback: string): string {
|
||||
if (typeof err === "string" && err.trim()) return err.trim();
|
||||
|
||||
if (err && typeof err === "object") {
|
||||
const e = err as Record<string, unknown>;
|
||||
const responseData = (e["response"] as Record<string, unknown> | undefined)?.["data"];
|
||||
if (responseData && typeof responseData === "object") {
|
||||
const rd = responseData as Record<string, unknown>;
|
||||
if (typeof rd["message"] === "string" && rd["message"].trim()) return rd["message"];
|
||||
if (Array.isArray(rd["message"])) return (rd["message"] as string[]).join(" — ");
|
||||
if (typeof rd["error"] === "string" && rd["error"].trim()) return rd["error"];
|
||||
}
|
||||
if (typeof e["message"] === "string" && e["message"].trim()) return e["message"];
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// ── Status badge ─────────────────────────────────────────────────────────────
|
||||
|
||||
function StatusBadge({ status }: { status: TripStatus }) {
|
||||
const s = TRIP_STATUS_MAP[status];
|
||||
return (
|
||||
<span style={{
|
||||
display: "inline-flex", alignItems: "center", gap: 5,
|
||||
padding: "3px 10px", borderRadius: 999,
|
||||
fontSize: 11, fontWeight: 700, letterSpacing: "0.02em",
|
||||
color: s.color, background: s.bg, border: `1px solid ${s.border}`,
|
||||
}}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: s.dot, flexShrink: 0 }} />
|
||||
{s.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Page ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function TripsPage() {
|
||||
const router = useRouter();
|
||||
const {
|
||||
trips, total, pages, loading, error,
|
||||
page, setPage,
|
||||
search, handleSearch,
|
||||
status, handleStatusFilter,
|
||||
reload,
|
||||
} = useTrips();
|
||||
|
||||
// ── Notifications (standalone — fired explicitly by every action below) ──
|
||||
const [notification, setNotification] = useState<ToastNotification | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const notify = useCallback((n: ToastNotification) => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
setNotification(n);
|
||||
timerRef.current = setTimeout(() => setNotification(null), 4000);
|
||||
}, []);
|
||||
|
||||
const dismissNotification = useCallback(() => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
setNotification(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
|
||||
|
||||
// Local input value is debounced before it reaches the hook's `search`,
|
||||
// exactly like the table reloads only after the user stops typing.
|
||||
const [searchInput, setSearchInput] = useState(search);
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => handleSearch(searchInput), 400);
|
||||
return () => clearTimeout(t);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchInput]);
|
||||
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editTrip, setEditTrip] = useState<Trip | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Trip | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// ── Create / update — calls tripService directly and notifies explicitly ──
|
||||
const handleSubmit = async (
|
||||
payload: CreateTripPayload | UpdateTripPayload,
|
||||
isNew: boolean,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
if (isNew) {
|
||||
await tripService.create(payload as CreateTripPayload, token);
|
||||
notify({ type: "success", message: "تم إضافة الرحلة بنجاح." });
|
||||
} else {
|
||||
await tripService.update(editTrip!.id, payload as UpdateTripPayload, token);
|
||||
notify({ type: "success", message: "تم تحديث بيانات الرحلة بنجاح." });
|
||||
}
|
||||
reload();
|
||||
return true;
|
||||
} catch (err) {
|
||||
notify({
|
||||
type: "error",
|
||||
message: extractApiMessage(err, isNew ? "تعذّر إضافة الرحلة." : "تعذّر تحديث الرحلة."),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
await tripService.delete(deleteTarget.id, token);
|
||||
notify({ type: "success", message: "تم حذف الرحلة بنجاح." });
|
||||
setDeleteTarget(null);
|
||||
reload();
|
||||
} catch (err) {
|
||||
notify({ type: "error", message: extractApiMessage(err, "تعذّر حذف الرحلة.") });
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div dir="rtl" style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1.25rem" }}>
|
||||
|
||||
{/* ── Header ── */}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: "0.75rem" }}>
|
||||
<div>
|
||||
<h1 style={{ fontSize: 20, fontWeight: 800, color: "var(--color-text-primary)", margin: 0 }}>الرحلات</h1>
|
||||
<p style={{ margin: "2px 0 0", fontSize: 12, color: "var(--color-text-muted)" }}>
|
||||
إدارة جدولة الرحلات وتتبع حالتها
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setEditTrip(null); setShowForm(true); }}
|
||||
style={{
|
||||
height: 40, padding: "0 1.25rem",
|
||||
borderRadius: "var(--radius-md)", border: "none",
|
||||
background: "var(--color-brand-600)", color: "#FFF",
|
||||
fontSize: 13, fontWeight: 700, cursor: "pointer",
|
||||
display: "flex", alignItems: "center", gap: 8,
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
إضافة رحلة
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Filters ── */}
|
||||
<div style={{
|
||||
display: "flex", gap: "0.75rem", flexWrap: "wrap",
|
||||
background: "var(--color-surface)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: "var(--radius-lg)",
|
||||
padding: "0.875rem 1rem",
|
||||
}}>
|
||||
<input
|
||||
value={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
placeholder="بحث برقم الرحلة أو العنوان…"
|
||||
dir="rtl"
|
||||
style={{
|
||||
flex: 1, minWidth: 200, height: 38, padding: "0 0.75rem",
|
||||
borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface-muted)", fontSize: 13,
|
||||
color: "var(--color-text-primary)", fontFamily: "var(--font-sans)", outline: "none",
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
value={status}
|
||||
onChange={e => handleStatusFilter(e.target.value as TripStatus | "")}
|
||||
dir="rtl"
|
||||
style={{
|
||||
height: 38, padding: "0 0.75rem", minWidth: 140,
|
||||
borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface-muted)", fontSize: 13,
|
||||
color: "var(--color-text-primary)", fontFamily: "var(--font-sans)",
|
||||
cursor: "pointer", outline: "none",
|
||||
}}
|
||||
>
|
||||
<option value="">كل الحالات</option>
|
||||
<option value="Scheduled">مجدولة</option>
|
||||
<option value="InProgress">جارية</option>
|
||||
<option value="Completed">مكتملة</option>
|
||||
<option value="Cancelled">ملغاة</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* ── Table card ── */}
|
||||
<div style={{
|
||||
background: "var(--color-surface)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: "var(--radius-xl)",
|
||||
overflow: "hidden",
|
||||
boxShadow: "0 1px 4px rgba(0,0,0,.06)",
|
||||
}}>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ padding: "3rem", display: "flex", justifyContent: "center" }}>
|
||||
<Spinner size="md" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div style={{ padding: "2rem", textAlign: "center", color: "var(--color-danger)", fontSize: 13 }}>
|
||||
⚠ {error}
|
||||
</div>
|
||||
) : trips.length === 0 ? (
|
||||
<div style={{ padding: "3rem", textAlign: "center", color: "var(--color-text-muted)", fontSize: 13 }}>
|
||||
لا توجد رحلات مطابقة
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ background: "var(--color-surface-muted)", borderBottom: "1px solid var(--color-border)" }}>
|
||||
{["رقم الرحلة", "العنوان", "السائق", "السيارة", "الحالة", "البدء", "الانتهاء", "إجراءات"].map(h => (
|
||||
<th key={h} style={{
|
||||
padding: "0.75rem 1rem", textAlign: "right", fontWeight: 700,
|
||||
color: "var(--color-text-secondary)", fontSize: 11,
|
||||
letterSpacing: "0.03em", whiteSpace: "nowrap",
|
||||
}}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trips.map((trip, i) => (
|
||||
<tr
|
||||
key={trip.id}
|
||||
onClick={() => router.push(`/dashboard/trips/${trip.id}`)}
|
||||
style={{
|
||||
borderBottom: i < trips.length - 1 ? "1px solid var(--color-border)" : "none",
|
||||
cursor: "pointer",
|
||||
transition: "background 0.15s",
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = "var(--color-surface-muted)")}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = "")}
|
||||
>
|
||||
<td style={{ padding: "0.875rem 1rem", whiteSpace: "nowrap" }}>
|
||||
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--color-brand-600)", fontWeight: 700 }}>
|
||||
{trip.tripNumber}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: "0.875rem 1rem", fontWeight: 600, color: "var(--color-text-primary)", maxWidth: 200 }}>
|
||||
<span style={{ display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{trip.title}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: "0.875rem 1rem", color: "var(--color-text-secondary)", whiteSpace: "nowrap" }}>
|
||||
{trip.driver?.name ?? "—"}
|
||||
</td>
|
||||
<td style={{ padding: "0.875rem 1rem", color: "var(--color-text-secondary)", whiteSpace: "nowrap" }}>
|
||||
{trip.car ? `${trip.car.manufacturer} ${trip.car.model}` : "—"}
|
||||
</td>
|
||||
<td style={{ padding: "0.875rem 1rem" }}>
|
||||
<StatusBadge status={trip.status} />
|
||||
</td>
|
||||
<td style={{ padding: "0.875rem 1rem", color: "var(--color-text-muted)", fontSize: 12, whiteSpace: "nowrap" }}>
|
||||
{trip.startTime ? new Date(trip.startTime).toLocaleString("ar-SA", { dateStyle: "short", timeStyle: "short" }) : "—"}
|
||||
</td>
|
||||
<td style={{ padding: "0.875rem 1rem", color: "var(--color-text-muted)", fontSize: 12, whiteSpace: "nowrap" }}>
|
||||
{trip.endTime ? new Date(trip.endTime).toLocaleString("ar-SA", { dateStyle: "short", timeStyle: "short" }) : "—"}
|
||||
</td>
|
||||
<td style={{ padding: "0.875rem 1rem" }} onClick={e => e.stopPropagation()}>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
onClick={() => { setEditTrip(trip); setShowForm(true); }}
|
||||
title="تعديل"
|
||||
style={{
|
||||
width: 30, height: 30, borderRadius: "var(--radius-md)",
|
||||
border: "1px solid var(--color-border)", background: "var(--color-surface)",
|
||||
cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center",
|
||||
color: "var(--color-text-muted)",
|
||||
}}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<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
|
||||
onClick={() => setDeleteTarget(trip)}
|
||||
title="حذف"
|
||||
style={{
|
||||
width: 30, height: 30, borderRadius: "var(--radius-md)",
|
||||
border: "1px solid #FECACA", background: "#FEF2F2",
|
||||
cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center",
|
||||
color: "#DC2626",
|
||||
}}
|
||||
>
|
||||
<svg width="13" height="13" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Pagination ── */}
|
||||
{pages > 1 && (
|
||||
<div style={{
|
||||
padding: "0.875rem 1rem",
|
||||
borderTop: "1px solid var(--color-border)",
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
background: "var(--color-surface-muted)",
|
||||
}}>
|
||||
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
|
||||
{total} رحلة — صفحة {page} من {pages}
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
{[...Array(pages)].map((_, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setPage(idx + 1)}
|
||||
style={{
|
||||
width: 32, height: 32, borderRadius: "var(--radius-md)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: page === idx + 1 ? "var(--color-brand-600)" : "var(--color-surface)",
|
||||
color: page === idx + 1 ? "#FFF" : "var(--color-text-secondary)",
|
||||
fontSize: 13, fontWeight: 600, cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{idx + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Modals ── */}
|
||||
{showForm && (
|
||||
<TripFormModal
|
||||
editTrip={editTrip}
|
||||
onClose={() => setShowForm(false)}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
)}
|
||||
{deleteTarget && (
|
||||
<TripDeleteModal
|
||||
trip={deleteTarget}
|
||||
deleting={deleting}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Toast ── */}
|
||||
<Toast notification={notification} onDismiss={dismissNotification} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user