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:
@@ -1,9 +1,18 @@
|
||||
"use client";
|
||||
|
||||
// app/dashboard/trips/[tripId]/page.tsx
|
||||
// CHANGE: TripFormModal now imported from the barrel (@/src/Components/Trip)
|
||||
// instead of a direct path — avoids the Tripformmodal.tsx case-sensitivity
|
||||
// mismatch that breaks on case-sensitive filesystems (Linux/Docker builds).
|
||||
// CHANGE: loadTrip's catch block now uses the same extractApiMessage helper
|
||||
// already defined in this file (and used by handleEditSubmit/handleConfirmDelete)
|
||||
// instead of `err instanceof Error ? err.message : ...` — so backend
|
||||
// validation messages surface correctly here too, not just on edit/delete.
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { ConfirmDialog, Spinner } from "@/src/Components/UI";
|
||||
import { TripFormModal } from "@/src/Components/Trip/Tripformmodal";
|
||||
import { TripFormModal } from "@/src/Components/Trip";
|
||||
import { TripReportPanel } from "@/src/Components/Trip_Report/Tripreportpanel";
|
||||
import { tripService } from "@/src/services/trip.service";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
@@ -205,42 +214,42 @@ export default function TripDetailPage() {
|
||||
|
||||
// ── Load trip ─────────────────────────────────────────────────────────────
|
||||
const loadTrip = useCallback(async () => {
|
||||
if (!tripId) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
const data = await tripService.getById(tripId, token);
|
||||
setTrip(data);
|
||||
} catch (err: unknown) {
|
||||
setError(err instanceof Error ? err.message : "تعذّر تحميل بيانات الرحلة.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [tripId]);
|
||||
if (!tripId) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
const data = await tripService.getById(tripId, token);
|
||||
setTrip(data);
|
||||
} catch (err: unknown) {
|
||||
setError(extractApiMessage(err, "تعذّر تحميل بيانات الرحلة."));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [tripId]);
|
||||
useEffect(() => {
|
||||
queueMicrotask(loadTrip);
|
||||
}, [loadTrip]);
|
||||
|
||||
// ── Edit submit ───────────────────────────────────────────────────────────
|
||||
const handleEditSubmit = useCallback(
|
||||
async (
|
||||
payload: CreateTripPayload | UpdateTripPayload,
|
||||
): Promise<boolean> => {
|
||||
if (!trip) return false;
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
const updated = await tripService.update(trip.id, payload as UpdateTripPayload, token);
|
||||
setTrip(updated);
|
||||
notify({ type: "success", message: "تم تحديث بيانات الرحلة بنجاح." });
|
||||
return true;
|
||||
} catch (err) {
|
||||
notify({ type: "error", message: extractApiMessage(err, "تعذّر تحديث الرحلة.") });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[trip, notify],
|
||||
);
|
||||
async (
|
||||
payload: CreateTripPayload | UpdateTripPayload,
|
||||
): Promise<boolean> => {
|
||||
if (!trip) return false;
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
const updated = await tripService.update(trip.id, payload as UpdateTripPayload, token);
|
||||
setTrip(updated);
|
||||
notify({ type: "success", message: "تم تحديث بيانات الرحلة بنجاح." });
|
||||
return true;
|
||||
} catch (err) {
|
||||
notify({ type: "error", message: extractApiMessage(err, "تعذّر تحديث الرحلة.") });
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[trip, notify],
|
||||
);
|
||||
|
||||
// ── Delete confirm ────────────────────────────────────────────────────────
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
|
||||
@@ -1,110 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { Spinner, EmptyState, Badge, PageHeader } from "@/src/Components/UI";
|
||||
import { tripService } from "@/src/services/trip.service";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
import type { Trip } from "@/src/types/trip";
|
||||
import { TRIP_STATUS_MAP } from "@/src/types/trip";
|
||||
import { ArchivedTripDetailModal } from "@/src/Components/Trip/archive/ArchivedtripDetailModal";
|
||||
// app/dashboard/trips/archived/[tripId]/page.tsx
|
||||
// CHANGE: rebuilt as a thin route wrapper around the shared
|
||||
// ArchivedTripDetailModal instead of its own independent fetch/loading/error/
|
||||
// render logic. This route now shows the exact same full detail view as the
|
||||
// in-app archive browser (driver + car + counts + cash + notes + endReason,
|
||||
// not just the handful of fields this page used to render on its own), and
|
||||
// stays in sync automatically with any future changes to that component.
|
||||
// `onClose` navigates back to the trips list (via router.push) instead of
|
||||
// closing an in-place modal, since this route has no page behind it to
|
||||
// reveal.
|
||||
|
||||
function fmtDateTime(iso?: string | null): string {
|
||||
if (!iso) return "—";
|
||||
return new Date(iso).toLocaleString("ar-SA", { dateStyle: "medium", timeStyle: "short" });
|
||||
}
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
||||
export default function ArchivedTripDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const tripId = params?.tripId as string;
|
||||
|
||||
const [trip, setTrip] = useState<Trip | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!tripId) return;
|
||||
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const token = getStoredToken();
|
||||
const data = await tripService.getArchivedById(tripId, token);
|
||||
if (!cancelled) setTrip(data);
|
||||
} catch {
|
||||
if (!cancelled) setError("لم يتم العثور على هذه الرحلة في الأرشيف.");
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => { cancelled = true; };
|
||||
}, [tripId]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3 py-24 text-[var(--color-text-muted)]">
|
||||
<Spinner size="sm" />
|
||||
<span className="text-sm">جارٍ التحميل…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !trip) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon="🗄️"
|
||||
title="الرحلة غير موجودة في الأرشيف"
|
||||
description={error ?? "تعذّر العثور على رحلة بهذا المعرّف."}
|
||||
action={
|
||||
<button
|
||||
onClick={() => router.push("/dashboard/trips")}
|
||||
className="text-sm font-semibold text-[var(--color-brand-600)] underline"
|
||||
>
|
||||
العودة إلى قائمة الرحلات
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const statusConfig = TRIP_STATUS_MAP[trip.status];
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-6 p-6" dir="rtl">
|
||||
<PageHeader
|
||||
title={trip.title}
|
||||
description={`رقم الرحلة: ${trip.tripNumber}`}
|
||||
backHref="/dashboard/trips"
|
||||
backLabel="الرحلات"
|
||||
action={<Badge label="مؤرشفة" color="amber" />}
|
||||
/>
|
||||
|
||||
<div className="rounded-[var(--radius-xl)] border border-[var(--color-border)] bg-[var(--color-surface)] p-6 shadow-[var(--shadow-card)]">
|
||||
<Badge label={statusConfig.label} color="slate" />
|
||||
<dl className="mt-4 grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<dt className="text-[var(--color-text-muted)]">وقت البدء</dt>
|
||||
<dd>{fmtDateTime(trip.startTime)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-[var(--color-text-muted)]">وقت الانتهاء</dt>
|
||||
<dd>{fmtDateTime(trip.endTime)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-[var(--color-text-muted)]">تاريخ الأرشفة</dt>
|
||||
<dd>{fmtDateTime(trip.deletedAt)}</dd>
|
||||
</div>
|
||||
{trip.driver && (
|
||||
<div>
|
||||
<dt className="text-[var(--color-text-muted)]">السائق</dt>
|
||||
<dd>{trip.driver.name}</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
<ArchivedTripDetailModal
|
||||
tripId={tripId}
|
||||
onClose={() => router.push("/dashboard/trips")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,164 +1,31 @@
|
||||
"use client";
|
||||
|
||||
// app/dashboard/trips/page.tsx
|
||||
// CHANGE: replaced the old hand-rolled inline table with <TripTable/>.
|
||||
// CHANGE: TripFormModal now imported from the barrel (@/src/Components/Trip)
|
||||
// instead of a direct path — avoids the Tripformmodal.tsx case-sensitivity
|
||||
// mismatch that breaks on case-sensitive filesystems (Linux/Docker builds).
|
||||
// CHANGE: removed the local ArchivedTripsModal wrapper (it duplicated what
|
||||
// the new ArchivedTripModal already does — search bar + table + detail) and
|
||||
// removed the ArchivedTripList import; now renders <ArchivedTripModal/>
|
||||
// directly.
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTrips } from "@/src/hooks/useTrip";
|
||||
import { TripFormModal } from "@/src/Components/Trip/Tripformmodal";
|
||||
import { ArchivedTripList } from "@/src/Components/Trip/archive/ArchivedTripList";
|
||||
import { Alert, Spinner, ArchiveButton, ConfirmDialog , Toast } from "@/src/Components/UI";
|
||||
import type {
|
||||
Trip,
|
||||
TripStatus,
|
||||
CreateTripPayload,
|
||||
UpdateTripPayload,
|
||||
} from "@/src/types/trip";
|
||||
import { TRIP_STATUS_MAP } from "@/src/types/trip";
|
||||
import { TripFormModal, TripTable } from "@/src/Components/Trip";
|
||||
import { ArchivedTripModal } from "@/src/Components/Trip/archive/ArchivedTripModal";
|
||||
import { Alert, ArchiveButton, ConfirmDialog, Toast } from "@/src/Components/UI";
|
||||
import type { Trip, CreateTripPayload, UpdateTripPayload } from "@/src/types/trip";
|
||||
import Header from "@/src/Components/UI/Header";
|
||||
|
||||
// ── Status badge — same visual pattern as Driver's status badge ─────────────
|
||||
|
||||
function StatusBadge({ status }: { status: TripStatus }) {
|
||||
const s = TRIP_STATUS_MAP[status];
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Archived trips modal — unchanged wrapper, kept as-is ────────────────────
|
||||
|
||||
function ArchivedTripsModal({ onClose }: { onClose: () => void }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const h = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
window.addEventListener("keydown", h);
|
||||
return () => window.removeEventListener("keydown", h);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="archived-trips-title"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 70,
|
||||
background: "rgba(15,23,42,0.6)",
|
||||
backdropFilter: "blur(4px)",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "center",
|
||||
padding: "2rem 1rem",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 920,
|
||||
background: "var(--color-surface-sunken)",
|
||||
borderRadius: "var(--radius-xl)",
|
||||
border: "1px solid var(--color-border)",
|
||||
boxShadow: "0 24px 64px rgba(0,0,0,.2)",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
dir="rtl"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "1.25rem 1.5rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
id="archived-trips-title"
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: 700,
|
||||
color: "var(--color-text-primary)",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
أرشيف الرحلات
|
||||
</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "1.5rem" }}>
|
||||
{/* Was pointing at the normal trip page — send it to the archived route instead */}
|
||||
<ArchivedTripList
|
||||
onView={(trip) =>
|
||||
router.push(`/dashboard/trips/archived/${trip.id}`)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Page ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function TripsPage() {
|
||||
const router = useRouter();
|
||||
|
||||
// All list state + mutations now come from the hook, same as useDrivers
|
||||
// is used on the Driver page. No direct tripService calls in this file.
|
||||
// All list state + mutations come from the hook, same as useDrivers is
|
||||
// used on the Driver page. No direct tripService calls in this file.
|
||||
const {
|
||||
trips,
|
||||
total,
|
||||
@@ -169,8 +36,6 @@ export default function TripsPage() {
|
||||
setPage,
|
||||
search,
|
||||
handleSearch,
|
||||
status,
|
||||
handleStatusFilter,
|
||||
createTrip,
|
||||
updateTrip,
|
||||
deleteTrip,
|
||||
@@ -202,6 +67,19 @@ export default function TripsPage() {
|
||||
setDeleteTarget(trip);
|
||||
}, []);
|
||||
|
||||
// TripTable exposes a dedicated "view" action (icon button) instead of a
|
||||
// row click — wired to the trip detail page, same destination the old
|
||||
// row-click handler used to navigate to.
|
||||
const handleView = useCallback(
|
||||
(trip: Trip) => router.push(`/dashboard/trips/${trip.id}`),
|
||||
[router],
|
||||
);
|
||||
|
||||
const handleAdd = useCallback(() => {
|
||||
setEditTrip(null);
|
||||
setShowForm(true);
|
||||
}, []);
|
||||
|
||||
// ── Create / update — delegates to the hook (hook fires the toast) ───────
|
||||
const handleSubmit = async (
|
||||
payload: CreateTripPayload | UpdateTripPayload,
|
||||
@@ -233,298 +111,38 @@ export default function TripsPage() {
|
||||
}}
|
||||
>
|
||||
{/* ── Header ── */}
|
||||
<Header mainTitle="إدارة الرحلات"
|
||||
title="الرحلات"
|
||||
name="رحلة"
|
||||
state={{ total }}
|
||||
search={search}
|
||||
setSearch={handleSearch}
|
||||
module=""
|
||||
setModule={() => {}}
|
||||
setPage={setPage}
|
||||
isAudit={false}
|
||||
onAdd={() => { setEditTrip(null); setShowForm(true); }}/>
|
||||
<Header
|
||||
mainTitle="إدارة الرحلات"
|
||||
title="الرحلات"
|
||||
name="رحلة"
|
||||
state={{ total }}
|
||||
search={search}
|
||||
setSearch={handleSearch}
|
||||
module=""
|
||||
setModule={() => {}}
|
||||
setPage={setPage}
|
||||
isAudit={false}
|
||||
onAdd={handleAdd}
|
||||
/>
|
||||
|
||||
{/* ── Error handling: Alert banner for list/API errors ──
|
||||
Same pattern as the Driver page: a persistent banner tied to the
|
||||
hook's `error` state, dismissible via `clearError`. */}
|
||||
{error && <Alert type="error" message={error} onClose={clearError} />}
|
||||
|
||||
{/* ── Table ── */}
|
||||
<div
|
||||
style={{
|
||||
background: "var(--color-surface)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: "var(--radius-xl)",
|
||||
overflow: "hidden",
|
||||
boxShadow: "var(--shadow-card)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
dir="rtl"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1.5fr 2fr 1.3fr 1.3fr 1fr 1.1fr 1.1fr 0.8fr",
|
||||
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)",
|
||||
}}
|
||||
>
|
||||
<span>رقم الرحلة</span>
|
||||
<span>العنوان</span>
|
||||
<span>السائق</span>
|
||||
<span>السيارة</span>
|
||||
<span>الحالة</span>
|
||||
<span>البدء</span>
|
||||
<span>الانتهاء</span>
|
||||
<span>إجراءات</span>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
</div>
|
||||
) : trips.length === 0 ? (
|
||||
<p
|
||||
style={{
|
||||
textAlign: "center",
|
||||
padding: "4rem 0",
|
||||
fontSize: 13,
|
||||
color: "var(--color-text-muted)",
|
||||
}}
|
||||
>
|
||||
لا توجد رحلات مطابقة {search && `لـ "${search}"`}
|
||||
</p>
|
||||
) : (
|
||||
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
||||
{trips.map((trip, i) => (
|
||||
<li
|
||||
key={trip.id}
|
||||
onClick={() => router.push(`/dashboard/trips/${trip.id}`)}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns:
|
||||
"1.5fr 2fr 1.3fr 1.3fr 1fr 1.1fr 1.1fr 0.8fr",
|
||||
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",
|
||||
transition: "background 0.15s",
|
||||
}}
|
||||
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")
|
||||
}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: "var(--font-mono)",
|
||||
fontSize: 12,
|
||||
color: "var(--color-brand-600)",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{trip.tripNumber}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
color: "var(--color-text-primary)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{trip.title}
|
||||
</span>
|
||||
<span style={{ color: "var(--color-text-secondary)" }}>
|
||||
{trip.driver?.name ?? "—"}
|
||||
</span>
|
||||
<span style={{ color: "var(--color-text-secondary)" }}>
|
||||
{trip.car
|
||||
? `${trip.car.manufacturer} ${trip.car.model}`
|
||||
: "—"}
|
||||
</span>
|
||||
<StatusBadge status={trip.status} />
|
||||
<span
|
||||
style={{ fontSize: 12, color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{trip.startTime
|
||||
? new Date(trip.startTime).toLocaleString("ar-SA", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
})
|
||||
: "—"}
|
||||
</span>
|
||||
<span
|
||||
style={{ fontSize: 12, color: "var(--color-text-muted)" }}
|
||||
>
|
||||
{trip.endTime
|
||||
? new Date(trip.endTime).toLocaleString("ar-SA", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
})
|
||||
: "—"}
|
||||
</span>
|
||||
|
||||
{/* Inline row actions — stopPropagation so the click does not
|
||||
also trigger navigation to the detail page. */}
|
||||
<div
|
||||
style={{ display: "flex", gap: "0.4rem" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="تعديل الرحلة"
|
||||
title="تعديل"
|
||||
onClick={() => handleEdit(trip)}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "var(--radius-md)",
|
||||
cursor: "pointer",
|
||||
flexShrink: 0,
|
||||
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={() => handleDelete(trip)}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "var(--radius-md)",
|
||||
cursor: "pointer",
|
||||
flexShrink: 0,
|
||||
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" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{/* ── Pagination — same pattern as Driver page ── */}
|
||||
{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>
|
||||
{" — "}
|
||||
{total} رحلة
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||
{[
|
||||
{
|
||||
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
|
||||
key={btn.label}
|
||||
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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TripTable
|
||||
trips={trips}
|
||||
loading={loading}
|
||||
search={search}
|
||||
page={page}
|
||||
pages={pages}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onView={handleView}
|
||||
onAddFirst={handleAdd}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
|
||||
{/* ── Form modal ── */}
|
||||
{showForm && (
|
||||
@@ -549,10 +167,10 @@ export default function TripsPage() {
|
||||
description={`هل أنت متأكد من حذف رحلة ${deleteTarget?.title ?? ""} (${deleteTarget?.tripNumber ?? ""})؟ لا يمكن التراجع عن هذا الإجراء.`}
|
||||
/>
|
||||
|
||||
{/* ── Archive browser modal ── */}
|
||||
{archiveOpen && (
|
||||
<ArchivedTripsModal onClose={() => setArchiveOpen(false)} />
|
||||
)}
|
||||
{/* ── Archive browser modal — now the split ArchivedTripModal directly,
|
||||
no local wrapper needed since it already ships its own search bar,
|
||||
table, and detail-view wiring. ── */}
|
||||
{archiveOpen && <ArchivedTripModal onClose={() => setArchiveOpen(false)} />}
|
||||
|
||||
{/* ── Floating archive button — same component Driver page uses ── */}
|
||||
<ArchiveButton onClick={() => setArchiveOpen(true)} label="الأرشيف" />
|
||||
|
||||
Reference in New Issue
Block a user