"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import Link from "next/link"; import { Alert, Spinner } from "../UI"; import { getStoredToken } from "@/src/lib/auth"; import { clientService } from "@/src/services/client.service"; import { tripService } from "@/src/services/trip.service"; import { createOrderSchema, updateOrderSchema, } from "@/src/validations/order.validation"; import type { ValidationError } from "yup"; import type { Order, CreateOrderPayload, UpdateOrderPayload, PaymentMethod, PaymentStatus, } from "@/src/types/order"; import type { Client } from "@/src/types/client"; import type { Trip } from "@/src/types/trip"; import type { OrderSchemaErrors } from "@/src/validations/order.validation"; // ── Shared input / label styles — verbatim from DriverFormModal.tsx ────────── const inputBase: React.CSSProperties = { width: "100%", height: 40, padding: "0 0.75rem", borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, color: "var(--color-text-primary)", outline: "none", fontFamily: "var(--font-sans)", }; const labelStyle: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 6, fontSize: 12, fontWeight: 600, color: "var(--color-text-secondary)", }; const errorTextStyle: React.CSSProperties = { fontSize: 11, color: "var(--color-danger)", fontWeight: 500, }; const sectionHeadingStyle: React.CSSProperties = { fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "0.5rem 0 0", }; const optionalLabelStyle: React.CSSProperties = { fontSize: 10, fontWeight: 500, color: "var(--color-text-hint)", marginRight: 4, }; /** Small "Create new" link rendered below each relation dropdown. */ const createLinkStyle: React.CSSProperties = { fontSize: 11, color: "var(--color-brand-600)", textDecoration: "none", fontWeight: 500, marginTop: 2, display: "inline-flex", alignItems: "center", gap: 3, }; // ── Props ──────────────────────────────────────────────────────────────────── interface OrderFormModalProps { editOrder: Order | null; onClose: () => void; onSubmit: ( payload: CreateOrderPayload | UpdateOrderPayload, isNew: boolean, ) => Promise; } // ── Component ──────────────────────────────────────────────────────────────── export function OrderFormModal({ editOrder, onClose, onSubmit, }: OrderFormModalProps) { const isNew = editOrder === null; // ── Relation data — clients & trips loaded once on mount ───────────────── const [clients, setClients] = useState([]); const [trips, setTrips] = useState([]); const [relLoading, setRelLoading] = useState(true); // ── Form state ──────────────────────────────────────────────────────────── const [shipmentNumber, setShipmentNumber] = useState( editOrder?.shipmentNumber ?? "", ); const [recipientName, setRecipientName] = useState( editOrder?.recipientName ?? "", ); const [recipientPhone, setRecipientPhone] = useState( editOrder?.recipientPhone ?? "", ); const [clientId, setClientId] = useState(editOrder?.clientId ?? ""); const [tripId, setTripId] = useState(editOrder?.tripId ?? ""); const [type, setType] = useState(editOrder?.type ?? ""); const [quantity, setQuantity] = useState(String(editOrder?.quantity ?? 1)); const [weight, setWeight] = useState( editOrder?.weight != null ? String(editOrder.weight) : "", ); const [subTotal, setSubTotal] = useState( editOrder?.subTotal != null ? String(editOrder.subTotal) : "", ); const [vatRate, setVatRate] = useState( editOrder?.vatRate != null ? String(editOrder.vatRate) : "", ); const [paymentMethod, setPaymentMethod] = useState( editOrder?.paymentMethod ?? "", ); const [paymentStatus, setPaymentStatus] = useState( editOrder?.paymentStatus ?? "", ); // ── Address state ───────────────────────────────────────────────────────── // Delivery address (عنوان التسليم) — always creates new MongoDB doc const [delCity, setDelCity] = useState( editOrder?.deliveryAddress?.details?.city ?? "", ); const [delDistrict, setDelDistrict] = useState( editOrder?.deliveryAddress?.details?.district ?? "", ); const [delStreet, setDelStreet] = useState( editOrder?.deliveryAddress?.details?.street ?? "", ); const [delBuildingNo, setDelBuildingNo] = useState( editOrder?.deliveryAddress?.details?.buildingNo ?? "", ); const [delUnitNo, setDelUnitNo] = useState( editOrder?.deliveryAddress?.details?.unitNo ?? "", ); const [delZipCode, setDelZipCode] = useState( editOrder?.deliveryAddress?.details?.zipCode ?? "", ); // GeoJSON coordinates [longitude, latitude] — required by backend const [delLng, setDelLng] = useState( editOrder?.deliveryAddress?.location?.coordinates?.[0] != null ? String(editOrder.deliveryAddress!.location!.coordinates![0]) : "", ); const [delLat, setDelLat] = useState( editOrder?.deliveryAddress?.location?.coordinates?.[1] != null ? String(editOrder.deliveryAddress!.location!.coordinates![1]) : "", ); // Pickup address (عنوان الاستلام) — إما ID موجود أو object جديد const [pickupMode, setPickupMode] = useState<"id" | "new">( editOrder?.pickupAddressId ? "id" : "new", ); const [pickupAddressId, setPickupAddressId] = useState( editOrder?.pickupAddressId ?? "", ); const [pkpCity, setPkpCity] = useState( editOrder?.pickupAddress?.details?.city ?? "", ); const [pkpDistrict, setPkpDistrict] = useState( editOrder?.pickupAddress?.details?.district ?? "", ); const [pkpStreet, setPkpStreet] = useState( editOrder?.pickupAddress?.details?.street ?? "", ); const [pkpBuildingNo, setPkpBuildingNo] = useState( editOrder?.pickupAddress?.details?.buildingNo ?? "", ); const [pkpUnitNo, setPkpUnitNo] = useState( editOrder?.pickupAddress?.details?.unitNo ?? "", ); const [pkpZipCode, setPkpZipCode] = useState( editOrder?.pickupAddress?.details?.zipCode ?? "", ); // GeoJSON coordinates [longitude, latitude] — optional for pickup const [pkpLng, setPkpLng] = useState( editOrder?.pickupAddress?.location?.coordinates?.[0] != null ? String(editOrder.pickupAddress!.location!.coordinates![0]) : "", ); const [pkpLat, setPkpLat] = useState( editOrder?.pickupAddress?.location?.coordinates?.[1] != null ? String(editOrder.pickupAddress!.location!.coordinates![1]) : "", ); const [errors, setErrors] = useState({}); const [saving, setSaving] = useState(false); const [apiError, setApiError] = useState(""); const firstRef = useRef(null); // ── Load clients + trips in parallel on mount ───────────────────────────── useEffect(() => { let cancelled = false; (async () => { try { const token = getStoredToken(); // Fetch first 100 clients (enough for a practical dropdown) const clientRes = await clientService.getAll(1, "", token); const clientPayload = (clientRes as unknown as { data?: { data?: Client[] } }).data ?? clientRes; const clientList: Client[] = (clientPayload as { data?: Client[] }).data ?? []; // Fetch first 100 trips (active ones the order can be assigned to) const tripRes = await tripService.getAll( { page: 1, limit: 100 }, token, ); const tripPayload = (tripRes as unknown as { data?: { data?: Trip[] } }).data ?? tripRes; const tripList: Trip[] = (tripPayload as { data?: Trip[] }).data ?? []; if (!cancelled) { setClients(clientList); setTrips(tripList); } } catch (err) { // Logged so failures are visible during development instead of // failing completely silently when clientService/tripService error out // (e.g. expired token, CORS, 500). Dropdowns stay empty either way, // and the user can still navigate to the create pages via the helper // links — but now they also see a message explaining why. console.error("OrderFormModal: failed to load clients/trips", err); if (!cancelled) { setApiError( "تعذّر تحميل قوائم العملاء والرحلات. تحقق من اتصالك وحاول إعادة فتح النافذة.", ); } } finally { if (!cancelled) setRelLoading(false); } })(); return () => { cancelled = true; }; }, []); // ── Keyboard / focus setup ──────────────────────────────────────────────── useEffect(() => { firstRef.current?.focus(); }, []); useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]); // ── Dynamic input error style ───────────────────────────────────────────── const inputStyle = (field: keyof OrderSchemaErrors): React.CSSProperties => ({ ...inputBase, border: errors[field] ? "1px solid var(--color-danger)" : inputBase.border, ...(errors[field] ? { background: "#FEF2F2" } : {}), }); const clearFieldError = useCallback( (field: keyof OrderSchemaErrors) => setErrors((p) => ({ ...p, [field]: undefined })), [], ); // ── Yup validation ──────────────────────────────────────────────────────── const runValidation = useCallback(async (): Promise => { const schema = isNew ? createOrderSchema : updateOrderSchema; try { await schema.validate( { tripId, clientId, shipmentNumber, recipientName, recipientPhone, quantity: quantity ? Number(quantity) : undefined, weight: weight ? Number(weight) : undefined, subTotal: subTotal ? Number(subTotal) : undefined, vatRate: vatRate ? Number(vatRate) : undefined, paymentMethod: paymentMethod || undefined, paymentStatus: paymentStatus || undefined, type: type || undefined, }, { abortEarly: false }, ); setErrors({}); return true; } catch (err) { const bag: OrderSchemaErrors = {}; (err as ValidationError).inner.forEach((e) => { if (e.path) (bag as Record)[e.path] = e.message; }); setErrors(bag); return false; } }, [ isNew, tripId, clientId, shipmentNumber, recipientName, recipientPhone, quantity, weight, subTotal, vatRate, paymentMethod, paymentStatus, type, ]); // ── Submit ──────────────────────────────────────────────────────────────── const handleSubmit = async (e: React.FormEvent) => { console.log("her"); e.preventDefault(); const valid = await runValidation(); if (!valid) return; // Build clean payload. // NOTE: deliveryAddressId / pickupAddressId are intentionally omitted // from create payloads — the backend creates address records from // deliveryAddressData / pickupAddressData instead. const payload: Record = { shipmentNumber, recipientName, recipientPhone, clientId, quantity: Number(quantity) || 1, }; // tripId: only include when a value is selected if (tripId) payload.tripId = tripId; // Optional fields — include only when filled if (type) payload.type = type; if (weight) payload.weight = Number(weight); if (subTotal) payload.subTotal = Number(subTotal); if (vatRate) payload.vatRate = Number(vatRate); if (paymentMethod) payload.paymentMethod = paymentMethod; if (paymentStatus) payload.paymentStatus = paymentStatus; // ── Build delivery address object (only if at least one field filled) ── const delDetails = { ...(delCity && { city: delCity }), ...(delDistrict && { district: delDistrict }), ...(delStreet && { street: delStreet }), ...(delBuildingNo && { buildingNo: delBuildingNo }), ...(delUnitNo && { unitNo: delUnitNo }), ...(delZipCode && { zipCode: delZipCode }), }; // coordinates are always included when provided (required by backend GeoJSON schema) const delCoordinates = delLng.trim() && delLat.trim() ? [Number(delLng), Number(delLat)] : undefined; if (Object.keys(delDetails).length > 0 || delCoordinates) { payload.deliveryAddress = { ...(Object.keys(delDetails).length > 0 && { details: delDetails }), ...(delCoordinates && { location: { coordinates: delCoordinates } }), }; } // ── Build pickup address: ID or new object ── if (pickupMode === "id" && pickupAddressId.trim()) { payload.pickupAddressId = pickupAddressId.trim(); } else { const pkpDetails = { ...(pkpCity && { city: pkpCity }), ...(pkpDistrict && { district: pkpDistrict }), ...(pkpStreet && { street: pkpStreet }), ...(pkpBuildingNo && { buildingNo: pkpBuildingNo }), ...(pkpUnitNo && { unitNo: pkpUnitNo }), ...(pkpZipCode && { zipCode: pkpZipCode }), }; const pkpCoordinates = pkpLng.trim() && pkpLat.trim() ? [Number(pkpLng), Number(pkpLat)] : undefined; if (Object.keys(pkpDetails).length > 0 || pkpCoordinates) { payload.pickupAddress = { ...(Object.keys(pkpDetails).length > 0 && { details: pkpDetails }), ...(pkpCoordinates && { location: { coordinates: pkpCoordinates } }), }; } } setSaving(true); setApiError(""); try { const ok = await onSubmit( payload as unknown as CreateOrderPayload, isNew, ); if (ok) { onClose(); } else { setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); } } catch (err) { const message = err instanceof Error ? err.message : "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."; setApiError(message); } finally { setSaving(false); } }; // ── Render ──────────────────────────────────────────────────────────────── return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: "fixed", inset: 0, zIndex: 50, background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", overflowY: "auto", }} >
e.stopPropagation()} style={{ width: "100%", maxWidth: 640, background: "var(--color-surface)", borderRadius: "var(--radius-2xl)", border: "1px solid var(--color-border)", boxShadow: "0 24px 64px rgba(0,0,0,.18)", overflow: "hidden", margin: "auto", }} > {/* ── Header ── */}

{isNew ? "إنشاء طلب" : "تعديل طلب"}

{isNew ? "طلب جديد" : editOrder?.shipmentNumber}

{/* ── Form ── */}
{apiError && ( setApiError("")} /> )} {/* ── Section: Shipment & Recipient ── */}

بيانات الشحنة والمستلم

{/* Shipment number — required */} {/* ── Client dropdown — required ── */} {/* Recipient name — required */} {/* Recipient phone — required */} {/* ── Trip dropdown — required on create, optional on edit ── */}
{/* ── Section: Shipment details ── */}

تفاصيل الشحنة

{/* Type — optional */} {/* Quantity — required, defaults to 1 */} {/* Weight — optional */}
{/* ── Section: Payment ── */}

بيانات الدفع

{/* Subtotal — optional */} {/* VAT rate — optional */} {/* Payment method — optional */} {/* Payment status — edit-only, mirrors Driver's status-on-edit pattern */} {!isNew && ( )}
{/* ── Section: Delivery Address ── */}

عنوان التسليم

{/* GeoJSON coordinates — required by backend */}
{/* ── Section: Pickup Address ── */}

عنوان الاستلام

{/* Toggle: existing ID or new address */}
{(["id", "new"] as const).map((m) => ( ))}
{pickupMode === "id" ? ( ) : (
{/* GeoJSON coordinates — optional for pickup */}
)} {/* ── Actions ── */}
); }