Files
LogisicsApp_Client/src/Components/Order/OrderFormModal.tsx
2026-07-02 20:07:38 +03:00

1183 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<boolean>;
}
// ── Component ────────────────────────────────────────────────────────────────
export function OrderFormModal({
editOrder,
onClose,
onSubmit,
}: OrderFormModalProps) {
const isNew = editOrder === null;
// ── Relation data — clients & trips loaded once on mount ─────────────────
const [clients, setClients] = useState<Client[]>([]);
const [trips, setTrips] = useState<Trip[]>([]);
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<PaymentMethod | "">(
editOrder?.paymentMethod ?? "",
);
const [paymentStatus, setPaymentStatus] = useState<PaymentStatus | "">(
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<OrderSchemaErrors>({});
const [saving, setSaving] = useState(false);
const [apiError, setApiError] = useState("");
const firstRef = useRef<HTMLInputElement>(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<boolean> => {
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<string, string>)[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<string, unknown> = {
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 (
<div
role="dialog"
aria-modal="true"
aria-labelledby="order-modal-title"
onClick={(e) => {
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",
}}
>
<div
onClick={(e) => 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 ── */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "1.25rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
}}
>
<div>
<p
style={{
fontSize: 11,
letterSpacing: "0.3em",
textTransform: "uppercase",
color: "#2563EB",
fontWeight: 600,
margin: 0,
}}
>
{isNew ? "إنشاء طلب" : "تعديل طلب"}
</p>
<h2
id="order-modal-title"
style={{
fontSize: 17,
fontWeight: 700,
color: "var(--color-text-primary)",
margin: "4px 0 0",
fontFamily: "var(--font-mono)",
}}
>
{isNew ? "طلب جديد" : editOrder?.shipmentNumber}
</h2>
</div>
<button
type="button"
onClick={onClose}
aria-label="إغلاق"
style={{
width: 34,
height: 34,
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
cursor: "pointer",
fontSize: 18,
color: "var(--color-text-muted)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
×
</button>
</div>
{/* ── Form ── */}
<form
onSubmit={handleSubmit}
noValidate
style={{
padding: "1.5rem",
display: "flex",
flexDirection: "column",
gap: "1rem",
maxHeight: "75vh",
overflowY: "auto",
}}
dir="rtl"
>
{apiError && (
<Alert
type="error"
message={apiError}
onClose={() => setApiError("")}
/>
)}
{/* ── Section: Shipment & Recipient ── */}
<p style={sectionHeadingStyle}>بيانات الشحنة والمستلم</p>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "0.75rem",
}}
>
{/* Shipment number — required */}
<label style={labelStyle}>
رقم الشحنة *
<input
ref={firstRef}
style={inputStyle("shipmentNumber")}
value={shipmentNumber}
onChange={(e) => {
setShipmentNumber(e.target.value);
clearFieldError("shipmentNumber");
}}
placeholder="SHP-0001"
dir="ltr"
autoComplete="off"
/>
{errors.shipmentNumber && (
<span style={errorTextStyle}>{errors.shipmentNumber}</span>
)}
</label>
{/* ── Client dropdown — required ── */}
<label style={labelStyle}>
العميل *
<select
style={{
...inputStyle("clientId"),
cursor: relLoading ? "wait" : "pointer",
// Shift placeholder text right for RTL
paddingRight: "0.75rem",
}}
value={clientId}
disabled={relLoading}
onChange={(e) => {
setClientId(e.target.value);
clearFieldError("clientId");
}}
dir="rtl"
>
<option value="">
{relLoading ? "جارٍ التحميل…" : "اختر العميل"}
</option>
{clients.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
{c.phone ? `${c.phone}` : ""}
</option>
))}
</select>
{errors.clientId && (
<span style={errorTextStyle}>{errors.clientId}</span>
)}
{/* Helper link */}
<Link
href="/dashboard/clients"
style={createLinkStyle}
tabIndex={-1}
>
<svg
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
إنشاء عميل جديد
</Link>
</label>
{/* Recipient name — required */}
<label style={labelStyle}>
اسم المستلم *
<input
style={inputStyle("recipientName")}
value={recipientName}
onChange={(e) => {
setRecipientName(e.target.value);
clearFieldError("recipientName");
}}
placeholder="أحمد محمد"
dir="rtl"
/>
{errors.recipientName && (
<span style={errorTextStyle}>{errors.recipientName}</span>
)}
</label>
{/* Recipient phone — required */}
<label style={labelStyle}>
رقم جوال المستلم *
<input
style={inputStyle("recipientPhone")}
value={recipientPhone}
onChange={(e) => {
setRecipientPhone(e.target.value);
clearFieldError("recipientPhone");
}}
placeholder="05XXXXXXXX"
dir="ltr"
/>
{errors.recipientPhone && (
<span style={errorTextStyle}>{errors.recipientPhone}</span>
)}
</label>
{/* ── Trip dropdown — required on create, optional on edit ── */}
<label style={{ ...labelStyle, gridColumn: "1 / -1" }}>
{isNew ? "الرحلة *" : "الرحلة"}
{!isNew && <span style={optionalLabelStyle}>(اختياري)</span>}
<select
style={{
...inputStyle("tripId"),
cursor: relLoading ? "wait" : "pointer",
}}
value={tripId ?? ""}
disabled={relLoading}
onChange={(e) => {
setTripId(e.target.value);
clearFieldError("tripId");
}}
dir="rtl"
>
<option value="">
{relLoading ? "جارٍ التحميل…" : "اختر الرحلة"}
</option>
{trips.map((t) => (
<option key={t.id} value={t.id}>
{t.tripNumber}
{t.title ? `${t.title}` : ""}
{t.driver?.name ? ` (${t.driver.name})` : ""}
</option>
))}
</select>
{errors.tripId && (
<span style={errorTextStyle}>{errors.tripId}</span>
)}
{/* Helper link */}
<Link
href="/dashboard/trips"
style={createLinkStyle}
tabIndex={-1}
>
<svg
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
إنشاء رحلة جديدة
</Link>
</label>
</div>
{/* ── Section: Shipment details ── */}
<p style={sectionHeadingStyle}>تفاصيل الشحنة</p>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: "0.75rem",
}}
>
{/* Type — optional */}
<label style={labelStyle}>
نوع الشحنة
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={type}
onChange={(e) => setType(e.target.value)}
placeholder="عادي / مبرد"
dir="rtl"
/>
</label>
{/* Quantity — required, defaults to 1 */}
<label style={labelStyle}>
الكمية *
<input
style={inputStyle("quantity")}
type="number"
min={1}
value={quantity}
onChange={(e) => {
setQuantity(e.target.value);
clearFieldError("quantity");
}}
dir="ltr"
/>
{errors.quantity && (
<span style={errorTextStyle}>{errors.quantity}</span>
)}
</label>
{/* Weight — optional */}
<label style={labelStyle}>
الوزن (كجم)
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
type="number"
min={0}
step="0.01"
value={weight}
onChange={(e) => setWeight(e.target.value)}
dir="ltr"
/>
</label>
</div>
{/* ── Section: Payment ── */}
<p style={sectionHeadingStyle}>بيانات الدفع</p>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: "0.75rem",
}}
>
{/* Subtotal — optional */}
<label style={labelStyle}>
الإجمالي الفرعي
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputStyle("subTotal")}
type="number"
min={0}
step="0.01"
value={subTotal}
onChange={(e) => {
setSubTotal(e.target.value);
clearFieldError("subTotal");
}}
dir="ltr"
/>
{errors.subTotal && (
<span style={errorTextStyle}>{errors.subTotal}</span>
)}
</label>
{/* VAT rate — optional */}
<label style={labelStyle}>
نسبة الضريبة (%)
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputStyle("vatRate")}
type="number"
min={0}
step="0.01"
value={vatRate}
onChange={(e) => {
setVatRate(e.target.value);
clearFieldError("vatRate");
}}
dir="ltr"
/>
{errors.vatRate && (
<span style={errorTextStyle}>{errors.vatRate}</span>
)}
</label>
{/* Payment method — optional */}
<label style={labelStyle}>
طريقة الدفع
<span style={optionalLabelStyle}>(اختياري)</span>
<select
style={{ ...inputBase, cursor: "pointer" }}
value={paymentMethod}
onChange={(e) =>
setPaymentMethod(e.target.value as PaymentMethod | "")
}
dir="rtl"
>
<option value="">اختر الطريقة</option>
<option value="Cash">نقداً</option>
<option value="Card">بطاقة</option>
<option value="Prepaid">مدفوع مسبقاً</option>
</select>
</label>
{/* Payment status — edit-only, mirrors Driver's status-on-edit pattern */}
{!isNew && (
<label style={labelStyle}>
حالة الدفع
<select
style={{ ...inputBase, cursor: "pointer" }}
value={paymentStatus}
onChange={(e) =>
setPaymentStatus(e.target.value as PaymentStatus | "")
}
dir="rtl"
>
<option value=""></option>
<option value="Pending">معلَّق</option>
<option value="Paid">مدفوع</option>
<option value="Failed">فشل</option>
<option value="Refunded">مُسترجع</option>
</select>
</label>
)}
</div>
{/* ── Section: Delivery Address ── */}
<p style={sectionHeadingStyle}>عنوان التسليم</p>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: "0.75rem",
}}
>
<label style={labelStyle}>
المدينة
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={delCity}
onChange={(e) => setDelCity(e.target.value)}
placeholder="الرياض"
dir="rtl"
/>
</label>
<label style={labelStyle}>
الحي
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={delDistrict}
onChange={(e) => setDelDistrict(e.target.value)}
placeholder="العليا"
dir="rtl"
/>
</label>
<label style={labelStyle}>
الشارع
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={delStreet}
onChange={(e) => setDelStreet(e.target.value)}
placeholder="شارع الأمير محمد"
dir="rtl"
/>
</label>
<label style={labelStyle}>
رقم المبنى
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={delBuildingNo}
onChange={(e) => setDelBuildingNo(e.target.value)}
placeholder="1234"
dir="ltr"
/>
</label>
<label style={labelStyle}>
رقم الوحدة
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={delUnitNo}
onChange={(e) => setDelUnitNo(e.target.value)}
placeholder="56"
dir="ltr"
/>
</label>
<label style={labelStyle}>
الرمز البريدي
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={delZipCode}
onChange={(e) => setDelZipCode(e.target.value)}
placeholder="12345"
dir="ltr"
/>
</label>
{/* GeoJSON coordinates — required by backend */}
<label style={labelStyle}>
خط الطول (Longitude) *
<input
style={inputBase}
type="number"
step="any"
value={delLng}
onChange={(e) => setDelLng(e.target.value)}
placeholder="46.6753"
dir="ltr"
/>
</label>
<label style={labelStyle}>
خط العرض (Latitude) *
<input
style={inputBase}
type="number"
step="any"
value={delLat}
onChange={(e) => setDelLat(e.target.value)}
placeholder="24.7136"
dir="ltr"
/>
</label>
</div>
{/* ── Section: Pickup Address ── */}
<p style={sectionHeadingStyle}>عنوان الاستلام</p>
{/* Toggle: existing ID or new address */}
<div
style={{ display: "flex", gap: "0.5rem", marginBottom: "0.25rem" }}
>
{(["id", "new"] as const).map((m) => (
<button
key={m}
type="button"
onClick={() => setPickupMode(m)}
style={{
height: 32,
padding: "0 0.875rem",
borderRadius: "var(--radius-md)",
border: `1px solid ${pickupMode === m ? "var(--color-brand-600)" : "var(--color-border)"}`,
background:
pickupMode === m
? "var(--color-brand-50, #EFF6FF)"
: "var(--color-surface)",
fontSize: 12,
fontWeight: 600,
color:
pickupMode === m
? "var(--color-brand-600)"
: "var(--color-text-secondary)",
cursor: "pointer",
fontFamily: "var(--font-sans)",
}}
>
{m === "id" ? "عنوان موجود (ID)" : "عنوان جديد"}
</button>
))}
</div>
{pickupMode === "id" ? (
<label style={labelStyle}>
معرّف العنوان (MongoDB ID)
<input
style={inputBase}
value={pickupAddressId}
onChange={(e) => setPickupAddressId(e.target.value)}
placeholder="67a1b2c3d4e5f6a7b8c9d0e1"
dir="ltr"
autoComplete="off"
/>
</label>
) : (
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gap: "0.75rem",
}}
>
<label style={labelStyle}>
المدينة
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={pkpCity}
onChange={(e) => setPkpCity(e.target.value)}
placeholder="جدة"
dir="rtl"
/>
</label>
<label style={labelStyle}>
الحي
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={pkpDistrict}
onChange={(e) => setPkpDistrict(e.target.value)}
placeholder="الروضة"
dir="rtl"
/>
</label>
<label style={labelStyle}>
الشارع
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={pkpStreet}
onChange={(e) => setPkpStreet(e.target.value)}
placeholder="شارع التحلية"
dir="rtl"
/>
</label>
<label style={labelStyle}>
رقم المبنى
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={pkpBuildingNo}
onChange={(e) => setPkpBuildingNo(e.target.value)}
placeholder="4321"
dir="ltr"
/>
</label>
<label style={labelStyle}>
رقم الوحدة
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={pkpUnitNo}
onChange={(e) => setPkpUnitNo(e.target.value)}
placeholder="12"
dir="ltr"
/>
</label>
<label style={labelStyle}>
الرمز البريدي
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
value={pkpZipCode}
onChange={(e) => setPkpZipCode(e.target.value)}
placeholder="23456"
dir="ltr"
/>
</label>
{/* GeoJSON coordinates — optional for pickup */}
<label style={labelStyle}>
خط الطول (Longitude)
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
type="number"
step="any"
value={pkpLng}
onChange={(e) => setPkpLng(e.target.value)}
placeholder="46.6753"
dir="ltr"
/>
</label>
<label style={labelStyle}>
خط العرض (Latitude)
<span style={optionalLabelStyle}>(اختياري)</span>
<input
style={inputBase}
type="number"
step="any"
value={pkpLat}
onChange={(e) => setPkpLat(e.target.value)}
placeholder="24.7136"
dir="ltr"
/>
</label>
</div>
)}
{/* ── Actions ── */}
<div
style={{
display: "flex",
gap: "0.5rem",
justifyContent: "flex-end",
paddingTop: "0.5rem",
}}
>
<button
type="button"
onClick={onClose}
disabled={saving}
style={{
height: 40,
padding: "0 1.25rem",
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13,
fontWeight: 600,
color: "var(--color-text-secondary)",
cursor: saving ? "not-allowed" : "pointer",
fontFamily: "var(--font-sans)",
}}
>
إلغاء
</button>
<button
type="submit"
disabled={saving}
onClick={() => console.log("BUTTON CLICKED DIRECTLY")}
style={{
height: 40,
padding: "0 1.5rem",
borderRadius: "var(--radius-md)",
border: "none",
background: saving
? "var(--color-brand-400)"
: "var(--color-brand-600)",
fontSize: 13,
fontWeight: 700,
color: "#FFF",
cursor: saving ? "not-allowed" : "pointer",
display: "flex",
alignItems: "center",
gap: 8,
fontFamily: "var(--font-sans)",
}}
>
{saving && <Spinner size="sm" className="text-white" />}
{saving ? "جارٍ الحفظ…" : isNew ? "إنشاء الطلب" : "حفظ التغييرات"}
</button>
</div>
</form>
</div>
</div>
);
}