"use client";
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 { TripDeleteModal } from "@/src/Components/Trip/Tripdeletemodal";
import { ArchivedTripList } from "@/src/Components/Trip/archive/ArchivedTripList";
import { Alert, Spinner, ArchiveButton } from "@/src/Components/UI";
import { Toast } from "@/src/Components/UI/Toast";
import type {
Trip,
TripStatus,
CreateTripPayload,
UpdateTripPayload,
} from "@/src/types/trip";
import { TRIP_STATUS_MAP } from "@/src/types/trip";
// ── Status badge — same visual pattern as Driver's status badge ─────────────
function StatusBadge({ status }: { status: TripStatus }) {
const s = TRIP_STATUS_MAP[status];
return (
{s.label}
);
}
// ── 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 (
{
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",
}}
>
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",
}}
>
أرشيف الرحلات
×
// Was pointing at the normal trip page — send it to the archived
route instead
router.push(`/dashboard/trips/archived/${trip.id}`)
}
/>
);
}
// ── 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.
const {
trips,
total,
pages,
loading,
error,
page,
setPage,
search,
handleSearch,
status,
handleStatusFilter,
createTrip,
updateTrip,
deleteTrip,
notification,
dismissNotification,
clearError,
} = useTrips();
const [showForm, setShowForm] = useState(false);
const [editTrip, setEditTrip] = useState(null);
const [deleteTarget, setDeleteTarget] = useState(null);
const [deleting, setDeleting] = useState(false);
const [archiveOpen, setArchiveOpen] = useState(false);
// Debounced search input, same pattern as the Driver page's search box
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 handleEdit = useCallback((trip: Trip) => {
setEditTrip(trip);
setShowForm(true);
}, []);
const handleDelete = useCallback((trip: Trip) => {
setDeleteTarget(trip);
}, []);
// ── Create / update — delegates to the hook (hook fires the toast) ───────
const handleSubmit = async (
payload: CreateTripPayload | UpdateTripPayload,
isNew: boolean,
): Promise => {
if (isNew) {
return createTrip(payload as CreateTripPayload);
}
return updateTrip(editTrip!.id, payload as UpdateTripPayload);
};
// ── Delete confirm — delegates to the hook ────────────────────────────────
const handleConfirmDelete = async () => {
if (!deleteTarget) return;
setDeleting(true);
const ok = await deleteTrip(deleteTarget.id);
setDeleting(false);
if (ok) setDeleteTarget(null);
};
return (
{/* ── Header ── */}
إدارة العمليات
الرحلات
إجمالي{" "}
{total}
{" "}
رحلة مسجلة
{/* Search */}
setSearchInput(e.target.value)}
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)",
}}
/>
{/* Status filter */}
handleStatusFilter(e.target.value as TripStatus | "")
}
dir="rtl"
style={{
height: 40,
padding: "0 0.75rem",
minWidth: 140,
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13,
color: "var(--color-text-primary)",
fontFamily: "var(--font-sans)",
cursor: "pointer",
outline: "none",
}}
>
كل الحالات
مجدولة
جارية
مكتملة
ملغاة
{/* Archive button */}
setArchiveOpen(true)}
style={{
height: 40,
padding: "0 1.25rem",
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
color: "var(--color-text-secondary)",
fontSize: 13,
fontWeight: 700,
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: 8,
fontFamily: "var(--font-sans)",
}}
>
أرشيف الرحلات
{/* Add button */}
{
setEditTrip(null);
setShowForm(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)",
flexShrink: 0,
}}
>
إضافة رحلة
{/* ── 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 &&
}
{/* ── Table ── */}
رقم الرحلة
العنوان
السائق
السيارة
الحالة
البدء
الانتهاء
إجراءات
{loading ? (
جارٍ التحميل…
) : trips.length === 0 ? (
لا توجد رحلات مطابقة {search && `لـ "${search}"`}
) : (
{trips.map((trip, i) => (
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")
}
>
{trip.tripNumber}
{trip.title}
{trip.driver?.name ?? "—"}
{trip.car
? `${trip.car.manufacturer} ${trip.car.model}`
: "—"}
{trip.startTime
? new Date(trip.startTime).toLocaleString("ar-SA", {
dateStyle: "short",
timeStyle: "short",
})
: "—"}
{trip.endTime
? new Date(trip.endTime).toLocaleString("ar-SA", {
dateStyle: "short",
timeStyle: "short",
})
: "—"}
{/* Inline row actions — stopPropagation so the click does not
also trigger navigation to the detail page. */}
e.stopPropagation()}
>
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)",
}}
>
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",
}}
>
))}
)}
{/* ── Pagination — same pattern as Driver page ── */}
{pages > 1 && (
صفحة{" "}
{page}
{" "}
من{" "}
{pages}
{" — "}
{total} رحلة
{[
{
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) => (
{btn.label}
))}
)}
{/* ── Form modal ── */}
{showForm && (
setShowForm(false)}
onSubmit={async (payload, isNew) => {
const ok = await handleSubmit(payload, isNew);
if (ok) setShowForm(false);
return ok;
}}
/>
)}
{/* ── Delete modal ── */}
{deleteTarget && (
setDeleteTarget(null)}
onConfirm={handleConfirmDelete}
/>
)}
{/* ── Archive browser modal ── */}
{archiveOpen && (
setArchiveOpen(false)} />
)}
{/* ── Floating archive button — same component Driver page uses ── */}
setArchiveOpen(true)} label="الأرشيف" />
);
}