498 lines
18 KiB
TypeScript
498 lines
18 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, 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";
|
||
|
||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||
|
||
function fmtDate(iso?: string | null): string {
|
||
if (!iso) return "—";
|
||
return new Date(iso).toLocaleDateString("ar-SA", {
|
||
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");
|
||
}
|
||
|
||
// ── Sub-components ────────────────────────────────────────────────────────────
|
||
|
||
function SectionCard({ title, children }: { title: string; children: React.ReactNode }) {
|
||
return (
|
||
<div
|
||
style={{
|
||
borderRadius: "var(--radius-xl)",
|
||
border: "1px solid var(--color-border)",
|
||
background: "var(--color-surface)",
|
||
overflow: "hidden",
|
||
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,
|
||
}}>
|
||
{title}
|
||
</p>
|
||
</div>
|
||
<div style={{ padding: "1rem 1.5rem" }}>{children}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function DetailRow({
|
||
label,
|
||
value,
|
||
mono = false,
|
||
}: {
|
||
label: string;
|
||
value: string;
|
||
mono?: boolean;
|
||
}) {
|
||
return (
|
||
<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",
|
||
}}>
|
||
{value}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Stat card (للأعداد) ───────────────────────────────────────────────────────
|
||
|
||
function StatCard({
|
||
label,
|
||
value,
|
||
color = "var(--color-brand-600)",
|
||
bg = "var(--color-brand-50, #EFF6FF)",
|
||
}: {
|
||
label: string;
|
||
value: string;
|
||
color?: string;
|
||
bg?: 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)" }}>
|
||
{label}
|
||
</span>
|
||
<span style={{ fontSize: 22, fontWeight: 700, color, fontFamily: "var(--font-mono)" }}>
|
||
{value}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Page ──────────────────────────────────────────────────────────────────────
|
||
|
||
export default function TripDetailPage() {
|
||
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);
|
||
|
||
// ── Modal state ───────────────────────────────────────────────────────────
|
||
const [editOpen, setEditOpen] = useState(false);
|
||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||
const [deleting, setDeleting] = useState(false);
|
||
|
||
// ── Load trip ─────────────────────────────────────────────────────────────
|
||
const loadTrip = useCallback(async () => {
|
||
if (!tripId) return;
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const token = getStoredToken();
|
||
const res = await tripService.getById(tripId, token);
|
||
setTrip((res as unknown as { data: Trip }).data);
|
||
} catch (err: unknown) {
|
||
setError(err instanceof Error ? err.message : "تعذّر تحميل بيانات الرحلة.");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [tripId]);
|
||
|
||
useEffect(() => { loadTrip(); }, [loadTrip]);
|
||
|
||
// ── Edit submit ───────────────────────────────────────────────────────────
|
||
const handleEditSubmit = useCallback(
|
||
async (
|
||
payload: CreateTripPayload | UpdateTripPayload,
|
||
_isNew: boolean,
|
||
): Promise<boolean> => {
|
||
if (!trip) return false;
|
||
try {
|
||
const token = getStoredToken();
|
||
await tripService.update(trip.id, payload as UpdateTripPayload, token);
|
||
await loadTrip();
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
},
|
||
[trip, loadTrip],
|
||
);
|
||
|
||
// ── Delete ────────────────────────────────────────────────────────────────
|
||
const handleConfirmDelete = useCallback(async () => {
|
||
if (!trip) return;
|
||
setDeleting(true);
|
||
try {
|
||
const token = getStoredToken();
|
||
await tripService.delete(trip.id, token);
|
||
router.back();
|
||
} catch {
|
||
setDeleting(false);
|
||
setDeleteOpen(false);
|
||
}
|
||
}, [trip, router]);
|
||
|
||
const statusConfig = trip ? TRIP_STATUS_MAP[trip.status] : null;
|
||
|
||
// ── Loading ───────────────────────────────────────────────────────────────
|
||
if (loading) {
|
||
return (
|
||
<div style={{ display: "flex", justifyContent: "center", padding: "4rem" }}>
|
||
<Spinner size="lg" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Error ─────────────────────────────────────────────────────────────────
|
||
if (error || !trip) {
|
||
return (
|
||
<div style={{ padding: "2rem", textAlign: "center" }}>
|
||
<p style={{ fontSize: 14, color: "var(--color-danger)", marginBottom: "1rem" }}>
|
||
{error ?? "الرحلة غير موجودة."}
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => router.back()}
|
||
style={{
|
||
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",
|
||
fontFamily: "var(--font-sans)",
|
||
}}
|
||
>
|
||
← العودة
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<section style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
||
|
||
{/* ── 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>
|
||
|
||
{/* 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>
|
||
|
||
<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}
|
||
</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>
|
||
)}
|
||
{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>
|
||
)}
|
||
</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>
|
||
</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"
|
||
/>
|
||
<StatCard
|
||
label="النقد المحصّل"
|
||
value={fmtNumber(trip.totalCashCollected)}
|
||
color="#581C87"
|
||
bg="#FAF5FF"
|
||
/>
|
||
</div>
|
||
|
||
{/* ── Content grid ── */}
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1.5rem" }}>
|
||
|
||
{/* Basic Info */}
|
||
<SectionCard title="بيانات الرحلة">
|
||
<DetailRow label="رقم الرحلة" value={trip.tripNumber} mono />
|
||
<DetailRow label="العنوان" value={trip.title} />
|
||
<DetailRow label="الحالة" value={statusConfig?.label ?? trip.status} />
|
||
<DetailRow label="الفرع" value={trip.branch?.name ?? "—"} />
|
||
<DetailRow label="وقت البدء" value={fmtDate(trip.startTime)} />
|
||
<DetailRow label="وقت الانتهاء" value={fmtDate(trip.endTime)} />
|
||
<DetailRow label="سبب الإنهاء" value={trip.endReason ?? "—"} />
|
||
</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>
|
||
|
||
{/* 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>
|
||
|
||
{/* System Info */}
|
||
<SectionCard title="معلومات النظام">
|
||
<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",
|
||
}}>
|
||
<TripReportPanel tripId={trip.id} />
|
||
</div>
|
||
|
||
</section>
|
||
|
||
{/* ── Edit modal ── */}
|
||
{editOpen && (
|
||
<TripFormModal
|
||
editTrip={trip}
|
||
onClose={() => setEditOpen(false)}
|
||
onSubmit={handleEditSubmit}
|
||
/>
|
||
)}
|
||
|
||
{/* ── Delete modal ── */}
|
||
{deleteOpen && (
|
||
<TripDeleteModal
|
||
trip={trip}
|
||
deleting={deleting}
|
||
onCancel={() => setDeleteOpen(false)}
|
||
onConfirm={handleConfirmDelete}
|
||
/>
|
||
)}
|
||
</>
|
||
);
|
||
} |