"use client"; import { useEffect, useState } from "react"; import Link from "next/link"; import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import { Alert, Button, Input, Modal, Select } 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 { Order, CreateOrderPayload, UpdateOrderPayload, PaymentMethod, PaymentStatus, } from "@/src/types/order"; import type { Client } from "@/src/types/client"; import type { Trip } from "@/src/types/trip"; const sectionHeadingStyle: React.CSSProperties = { fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "0.5rem 0 0", }; const createLinkStyle: React.CSSProperties = { fontSize: 11, color: "var(--color-brand-600)", textDecoration: "none", fontWeight: 500, marginTop: 6, display: "inline-flex", alignItems: "center", gap: 3, }; // ── Form values shape ──────────────────────────────────────────────────────── // Nested address paths mirror the yup schema exactly (deliveryAddress.details.*, // deliveryAddress.location.coordinates.0/1, etc.) so register("a.b.c") dot-paths // validate against the matching nested schema node without any extra mapping. interface AddressFormValues { details: { city: string; district: string; street: string; buildingNo: string; unitNo: string; zipCode: string; }; location: { coordinates: [number | undefined, number | undefined]; }; } interface OrderFormValues { shipmentNumber: string; recipientName: string; recipientPhone: string; clientId: string; tripId: string; type: string; quantity: number | undefined; weight: number | undefined; subTotal: number | undefined; vatRate: number | undefined; paymentMethod: PaymentMethod | ""; paymentStatus: PaymentStatus | ""; deliveryAddress: AddressFormValues; pickupAddressId: string; pickupAddress: AddressFormValues; } const emptyAddress = (): AddressFormValues => ({ details: { city: "", district: "", street: "", buildingNo: "", unitNo: "", zipCode: "" }, location: { coordinates: [undefined, undefined] }, }); interface OrderFormModalProps { editOrder: Order | null; onClose: () => void; onSubmit: ( payload: CreateOrderPayload | UpdateOrderPayload, isNew: boolean, ) => Promise; } export function OrderFormModal({ editOrder, onClose, onSubmit, }: OrderFormModalProps) { const isNew = editOrder === null; const [clients, setClients] = useState([]); const [trips, setTrips] = useState([]); const [relLoading, setRelLoading] = useState(true); const [pickupMode, setPickupMode] = useState<"id" | "new">( editOrder?.pickupAddressId ? "id" : "new", ); const { register, handleSubmit, setError, setFocus, formState: { errors, isSubmitting }, } = useForm({ // Cast the schema itself — create/update schemas differ structurally in // which fields are required, so yup can't unify them into one type. resolver: yupResolver((isNew ? createOrderSchema : updateOrderSchema) as any), defaultValues: { shipmentNumber: editOrder?.shipmentNumber ?? "", recipientName: editOrder?.recipientName ?? "", recipientPhone: editOrder?.recipientPhone ?? "", clientId: editOrder?.clientId ?? "", tripId: editOrder?.tripId ?? "", type: editOrder?.type ?? "", quantity: editOrder?.quantity ?? 1, weight: editOrder?.weight ?? undefined, subTotal: editOrder?.subTotal ?? undefined, vatRate: editOrder?.vatRate ?? undefined, paymentMethod: editOrder?.paymentMethod ?? "", paymentStatus: editOrder?.paymentStatus ?? "", deliveryAddress: { details: { city: editOrder?.deliveryAddress?.details?.city ?? "", district: editOrder?.deliveryAddress?.details?.district ?? "", street: editOrder?.deliveryAddress?.details?.street ?? "", buildingNo: editOrder?.deliveryAddress?.details?.buildingNo ?? "", unitNo: editOrder?.deliveryAddress?.details?.unitNo ?? "", zipCode: editOrder?.deliveryAddress?.details?.zipCode ?? "", }, location: { coordinates: [ editOrder?.deliveryAddress?.location?.coordinates?.[0], editOrder?.deliveryAddress?.location?.coordinates?.[1], ], }, }, pickupAddressId: editOrder?.pickupAddressId ?? "", pickupAddress: { details: { city: editOrder?.pickupAddress?.details?.city ?? "", district: editOrder?.pickupAddress?.details?.district ?? "", street: editOrder?.pickupAddress?.details?.street ?? "", buildingNo: editOrder?.pickupAddress?.details?.buildingNo ?? "", unitNo: editOrder?.pickupAddress?.details?.unitNo ?? "", zipCode: editOrder?.pickupAddress?.details?.zipCode ?? "", }, location: { coordinates: [ editOrder?.pickupAddress?.location?.coordinates?.[0], editOrder?.pickupAddress?.location?.coordinates?.[1], ], }, }, }, }); const [apiError, setApiError] = useState(""); // register("shipmentNumber") already attaches its own ref — focusing via // setFocus avoids the ref collision a separate `ref={firstRef}` would cause. useEffect(() => { setFocus("shipmentNumber"); }, [setFocus]); useEffect(() => { let cancelled = false; (async () => { try { const token = getStoredToken(); const { items: clientList } = await clientService.getAll(1, "", token); const { items: tripList } = await tripService.getAll({ page: 1, limit: 100 }, token); if (!cancelled) { setClients(clientList); setTrips(tripList); } } catch (err) { console.error("OrderFormModal: failed to load clients/trips", err); if (!cancelled) { setApiError("تعذّر تحميل قوائم العملاء والرحلات. تحقق من اتصالك وحاول إعادة فتح النافذة."); } } finally { if (!cancelled) setRelLoading(false); } })(); return () => { cancelled = true; }; }, []); const numberField = ( field: | "quantity" | "weight" | "subTotal" | "vatRate", ) => register(field, { setValueAs: (v) => (v === "" || v === null ? undefined : Number(v)), }); // Coordinate fields use dot-path registration (matches the pattern already // used in Client_Adress/Addressformmodal.tsx for location.coordinates.0/1). const coordField = ( path: | "deliveryAddress.location.coordinates.0" | "deliveryAddress.location.coordinates.1" | "pickupAddress.location.coordinates.0" | "pickupAddress.location.coordinates.1", ) => register(path as never, { setValueAs: (v: string) => (v === "" || v === null ? undefined : Number(v)), }); // ── Submit ──────────────────────────────────────────────────────────────── // Address objects stay entirely optional at the schema level (see the // inline note in order.validation.ts), so — exactly like before the // refactor — we only attach deliveryAddress/pickupAddress to the payload // when the person actually filled in at least one field for them. const submitHandler = async (data: OrderFormValues) => { const payload: Record = { shipmentNumber: data.shipmentNumber, recipientName: data.recipientName, recipientPhone: data.recipientPhone, clientId: data.clientId, quantity: data.quantity || 1, }; if (data.tripId) payload.tripId = data.tripId; if (data.type) payload.type = data.type; if (data.weight !== undefined) payload.weight = data.weight; if (data.subTotal !== undefined) payload.subTotal = data.subTotal; if (data.vatRate !== undefined) payload.vatRate = data.vatRate; if (data.paymentMethod) payload.paymentMethod = data.paymentMethod; if (data.paymentStatus) payload.paymentStatus = data.paymentStatus; const delDetails = Object.fromEntries( Object.entries(data.deliveryAddress.details).filter(([, v]) => !!v), ); const [delLng, delLat] = data.deliveryAddress.location.coordinates; const delCoordinates = delLng !== undefined && delLat !== undefined ? [delLng, delLat] : undefined; if (Object.keys(delDetails).length > 0 || delCoordinates) { payload.deliveryAddress = { ...(Object.keys(delDetails).length > 0 && { details: delDetails }), ...(delCoordinates && { location: { coordinates: delCoordinates } }), }; } if (pickupMode === "id" && data.pickupAddressId.trim()) { payload.pickupAddressId = data.pickupAddressId.trim(); } else { const pkpDetails = Object.fromEntries( Object.entries(data.pickupAddress.details).filter(([, v]) => !!v), ); const [pkpLng, pkpLat] = data.pickupAddress.location.coordinates; const pkpCoordinates = pkpLng !== undefined && pkpLat !== undefined ? [pkpLng, pkpLat] : undefined; if (Object.keys(pkpDetails).length > 0 || pkpCoordinates) { payload.pickupAddress = { ...(Object.keys(pkpDetails).length > 0 && { details: pkpDetails }), ...(pkpCoordinates && { location: { coordinates: pkpCoordinates } }), }; } } setApiError(""); try { const ok = await onSubmit(payload as unknown as CreateOrderPayload, isNew); if (ok) { onClose(); } else { setError("shipmentNumber", { message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً." }); setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); } } catch (err) { const message = err instanceof Error ? err.message : "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."; setError("shipmentNumber", { message }); setApiError(message); } }; const deliveryErr = errors.deliveryAddress; const pickupErr = errors.pickupAddress; return ( } >
{errors.shipmentNumber?.type === "manual" && ( setApiError("")} /> )}

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

إنشاء عميل جديد
إنشاء رحلة جديدة

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

بيانات الدفع

{!isNew && ( )}

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

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

{(["id", "new"] as const).map((m) => ( ))}
{pickupMode === "id" ? ( ) : (
)}
); } // Re-exported for callers that only need the empty-address shape (e.g. tests). export { emptyAddress as emptyOrderAddress };