"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; const responseData = (e["response"] as Record | undefined)?.["data"]; if (responseData && typeof responseData === "object") { const rd = responseData as Record; 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 ( {s.label} ); } // ── 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(null); const timerRef = useRef | 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(null); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); // ── Create / update — calls tripService directly and notifies explicitly ── const handleSubmit = async ( payload: CreateTripPayload | UpdateTripPayload, isNew: boolean, ): Promise => { 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 (
{/* ── Header ── */}

الرحلات

إدارة جدولة الرحلات وتتبع حالتها

{/* ── Filters ── */}
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", }} />
{/* ── Table card ── */}
{loading ? (
) : error ? (
⚠ {error}
) : trips.length === 0 ? (
لا توجد رحلات مطابقة
) : (
{["رقم الرحلة", "العنوان", "السائق", "السيارة", "الحالة", "البدء", "الانتهاء", "إجراءات"].map(h => ( ))} {trips.map((trip, i) => ( 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 = "")} > ))}
{h}
{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" }) : "—"} e.stopPropagation()}>
)} {/* ── Pagination ── */} {pages > 1 && (
{total} رحلة — صفحة {page} من {pages}
{[...Array(pages)].map((_, idx) => ( ))}
)}
{/* ── Modals ── */} {showForm && ( setShowForm(false)} onSubmit={handleSubmit} /> )} {deleteTarget && ( setDeleteTarget(null)} onConfirm={handleDelete} /> )} {/* ── Toast ── */}
); }