diff --git a/app/dashboard/clients/[clientId]/addresses/[addressesId]/page.tsx b/app/dashboard/clients/[clientId]/addresses/[addressesId]/page.tsx new file mode 100644 index 0000000..5291e4c --- /dev/null +++ b/app/dashboard/clients/[clientId]/addresses/[addressesId]/page.tsx @@ -0,0 +1,339 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; + +import { Alert, ConfirmDialog, Toast } from "@/src/Components/UI"; +import { AddressFormModal } from "@/src/Components/Client"; +import { AddressDetails } from "@/src/Components/Client_Adress/AddressDetails"; +import { clientAddressService } from "@/src/services/clientAddress.service"; +import { clientService } from "@/src/services/client.service"; +import { getStoredToken } from "@/src/lib/auth"; + +import type { Client } from "@/src/types/client"; +import type { ClientAddress } from "@/src/types/client_adresses"; +import type { ToastNotification } from "@/src/Components/UI"; +import type { + CreateAddressFormValues, + UpdateAddressFormValues, +} from "@/src/validations/client_address.validator"; + +// ─── Page ────────────────────────────────────────────────────────────────── + +export default function AddressDetailPage() { + const params = useParams(); + const router = useRouter(); + + const clientId = (params?.clientId ?? params?.id) as string | undefined; + const addressId = params?.addressId as string | undefined; + + // ── Data state ─────────────────────────────────────────────────────────── + const [address, setAddress] = useState(null); + const [client, setClient] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // ── Modal state ────────────────────────────────────────────────────────── + const [editOpen, setEditOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const [deleting, setDeleting] = useState(false); + + // ── Toast ──────────────────────────────────────────────────────────────── + const [notification, setNotification] = useState(null); + + const notify = (n: ToastNotification) => { + setNotification(n); + setTimeout(() => setNotification(null), 4000); + }; + + // ── Fetch ──────────────────────────────────────────────────────────────── + useEffect(() => { + if (!clientId || !addressId) return; + setLoading(true); + + Promise.all([ + clientAddressService.getById(clientId, addressId, getStoredToken()), + clientService.getById(clientId, getStoredToken()), + ]) + .then(([addrRes, clientRes]) => { + const raw = (addrRes as any).data ?? addrRes; + setAddress({ ...raw, id: raw.id ?? raw._id }); + setClient(clientRes?.data); + }) + .catch(() => setError("تعذّر تحميل بيانات العنوان. يرجى المحاولة مجدداً.")) + .finally(() => setLoading(false)); + }, [clientId, addressId]); + + // ── Update ─────────────────────────────────────────────────────────────── + const handleUpdateSubmit = async ( + data: CreateAddressFormValues | UpdateAddressFormValues + ): Promise => { + if (!clientId || !addressId) return false; + try { + const res = await clientAddressService.update( + clientId, + addressId, + data as UpdateAddressFormValues, + getStoredToken() + ); + const raw = (res as any).data ?? res; + setAddress({ ...raw, id: raw.id ?? raw._id }); + notify({ type: "success", message: "تم تحديث العنوان بنجاح." }); + return true; + } catch (err) { + notify({ + type: "error", + message: err instanceof Error ? err.message : "تعذّر تحديث العنوان.", + }); + return false; + } + }; + + // ── Delete ─────────────────────────────────────────────────────────────── + const handleDeleteConfirm = async () => { + if (!clientId || !addressId || deleting) return; + setDeleting(true); + try { + await clientAddressService.delete(clientId, addressId, getStoredToken()); + notify({ type: "success", message: "تم حذف العنوان بنجاح." }); + // short delay so the toast shows before navigation + setTimeout(() => { + router.replace(`/dashboard/clients/${clientId}/addresses`); + }, 700); + } catch (err) { + notify({ + type: "error", + message: err instanceof Error ? err.message : "تعذّر حذف العنوان.", + }); + setDeleting(false); + } + }; + + // ── Guards ─────────────────────────────────────────────────────────────── + if (loading) { + return ( +
+
+
+ ); + } + + if (error || !address) { + return ( +
+ + +
+ ); + } + + // ── Render ──────────────────────────────────────────────────────────────── + return ( + <> + + + {/* Edit modal */} + {editOpen && ( + setEditOpen(false)} + onSubmit={handleUpdateSubmit} + /> + )} + + {/* Delete confirmation */} + { if (!deleting) setDeleteOpen(false); }} + onConfirm={handleDeleteConfirm} + title="حذف العميل" + description={`هل أنت متأكد من حذف ${address.label}؟ سيتم حذف جميع عناوينه أيضاً. لا يمكن التراجع عن هذا الإجراء.`} + /> + +
+ + {/* ── Page header ── */} +
+ {/* Back link */} + + +

+ تفاصيل العنوان +

+ +
+
+

+ {address.label} +

+ {client && ( +

+ {client.name} +

+ )} +
+ + {/* Action buttons */} +
+ + + +
+
+
+ + {/* ── Address details component ── */} + + +
+ + ); +} \ No newline at end of file diff --git a/app/dashboard/role_permission/page.tsx b/app/dashboard/role_permission/page.tsx new file mode 100644 index 0000000..cb2be10 --- /dev/null +++ b/app/dashboard/role_permission/page.tsx @@ -0,0 +1,7 @@ +import React from 'react' + +export default function page() { + return ( +
page
+ ) +} diff --git a/src/Components/Admen/admen.tsx b/src/Components/Admen/admen.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/Branch/DeleteConfirmModal.tsx b/src/Components/Branch/DeleteConfirmModal.tsx new file mode 100644 index 0000000..22dc4f9 --- /dev/null +++ b/src/Components/Branch/DeleteConfirmModal.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useEffect } from "react"; +import { Spinner } from "../UI"; +import type { Branch } from "@/src/types/branch"; + +interface DeleteConfirmModalProps { + branch: Branch; + deleting: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +export function DeleteConfirmModal({ branch, deleting, onCancel, onConfirm }: DeleteConfirmModalProps) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [onCancel]); + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem" }} + > +
e.stopPropagation()} + style={{ width: "100%", maxWidth: 400, background: "var(--color-surface)", borderRadius: "var(--radius-xl)", border: "1px solid #FECACA", boxShadow: "0 20px 48px rgba(0,0,0,.18)", padding: "2rem", display: "flex", flexDirection: "column", gap: "1rem", textAlign: "center" }} + > + {/* icon delete */} +
+ + + + +
+
+

حذف الفرع

+

+ هل أنت متأكد من حذف فرع {branch.name}؟ لا يمكن التراجع عن هذا الإجراء. +

+
+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/Components/Car_Maintanance/CarMaintananceDeleteModal.tsx b/src/Components/Car_Maintanance/CarMaintananceDeleteModal.tsx new file mode 100644 index 0000000..856150a --- /dev/null +++ b/src/Components/Car_Maintanance/CarMaintananceDeleteModal.tsx @@ -0,0 +1,91 @@ + "use client"; + +import { useEffect } from "react"; +import { Spinner } from "../UI"; +import { fmtCost } from "@/src/types/carMaintanance"; +import type { CarMaintenance } from "@/src/types/carMaintanance"; + +interface CarMaintenanceDeleteModalProps { + record: CarMaintenance; + deleting: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +export function CarMaintenanceDeleteModal({ + record, + deleting, + onCancel, + onConfirm, +}: CarMaintenanceDeleteModalProps) { + useEffect(() => { + const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; + window.addEventListener("keydown", h); + return () => window.removeEventListener("keydown", h); + }, [onCancel]); + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + style={{ + position: "fixed", inset: 0, zIndex: 60, + background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", + display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", + }} + > +
e.stopPropagation()} + style={{ + width: "100%", maxWidth: 420, + background: "var(--color-surface)", + borderRadius: "var(--radius-xl)", + border: "1px solid #FECACA", + boxShadow: "0 20px 48px rgba(0,0,0,.18)", + padding: "2rem", + display: "flex", flexDirection: "column", gap: "1rem", + textAlign: "center", + }} + > + {/* Icon */} +
+ + + + + + +
+ +
+

+ حذف سجل الصيانة +

+

+ هل أنت متأكد من حذف سجل{" "} + + {record.reason} + + {" "}( + + {fmtCost(record.cost)} + + )؟ لا يمكن التراجع عن هذا الإجراء. +

+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/Components/Client/Addressformmodal.tsx b/src/Components/Client/Addressformmodal.tsx new file mode 100644 index 0000000..44f0942 --- /dev/null +++ b/src/Components/Client/Addressformmodal.tsx @@ -0,0 +1,392 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Alert, Spinner } from "../UI"; +import type { + ClientAddress, + ClientAddressFormData, + ClientAddressFormErrors, +} from "@/src/types/client"; + +// ── Fixed styles ─────────────────────────────────────────────────────────── +const S = { + input: { + 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)", + } as React.CSSProperties, + label: { + display: "flex", + flexDirection: "column" as const, + gap: 6, + fontSize: 12, + fontWeight: 600, + color: "var(--color-text-secondary)", + } as React.CSSProperties, + errorText: { + fontSize: 11, + color: "var(--color-danger)", + fontWeight: 500, + } as React.CSSProperties, +}; + +// ── Validation ───────────────────────────────────────────────────────────── +function validate(data: ClientAddressFormData): ClientAddressFormErrors { + const e: ClientAddressFormErrors = {}; + if (!data.label.trim()) e.label = "النوع مطلوب"; + if (!data.street.trim()) e.street = "العنوان مطلوب"; + if (!data.city.trim()) e.city = "المدينة مطلوبة"; + if (!data.state.trim()) e.state = "المنطقة مطلوبة"; + if (!data.postalCode.trim()) e.postalCode = "الرمز البريدي مطلوب"; + if (!data.country.trim()) e.country = "الدولة مطلوبة"; + return e; +} + +const LABEL_PRESETS = ["فوترة", "شحن", "المقر الرئيسي", "فرع", "مستودع", "أخرى"]; + +// ── Props ────────────────────────────────────────────────────────────────── +interface AddressFormModalProps { + editAddress: ClientAddress | null; // null = create mode + onClose: () => void; + onSubmit: (data: ClientAddressFormData, isNew: boolean) => Promise; +} + +// ── Component ────────────────────────────────────────────────────────────── +export function AddressFormModal({ + editAddress, + onClose, + onSubmit, +}: AddressFormModalProps) { + const isNew = editAddress === null; + + const [form, setForm] = useState({ + label: editAddress?.label ?? "", + street: editAddress?.street ?? "", + city: editAddress?.city ?? "", + state: editAddress?.state ?? "", + postalCode: editAddress?.postalCode ?? "", + country: editAddress?.country ?? "المملكة العربية السعودية", + isPrimary: editAddress?.isPrimary ?? false, + }); + const [errors, setErrors] = useState({}); + const [saving, setSaving] = useState(false); + const [apiError, setApiError] = useState(""); + const firstRef = useRef(null); + + useEffect(() => { firstRef.current?.focus(); }, []); + useEffect(() => { + const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [onClose]); + + const set = (field: keyof ClientAddressFormData) => + (e: React.ChangeEvent) => { + setForm((p) => ({ ...p, [field]: e.target.value })); + if (errors[field as keyof ClientAddressFormErrors]) + setErrors((p) => ({ ...p, [field]: undefined })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const errs = validate(form); + if (Object.keys(errs).length) { setErrors(errs); return; } + setSaving(true); + setApiError(""); + const ok = await onSubmit(form, isNew); + setSaving(false); + if (ok) onClose(); + else setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); + }; + + const inputStyle = (field: keyof ClientAddressFormErrors): React.CSSProperties => ({ + ...S.input, + ...(errors[field] ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}), + }); + + 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", + }} + > +
e.stopPropagation()} + style={{ + width: "100%", + maxWidth: 520, + 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", + }} + > + {/* Header */} +
+
+

+ {isNew ? "إضافة عنوان" : "تعديل عنوان"} +

+

+ {isNew ? "عنوان جديد" : editAddress?.label} +

+
+ +
+ + {/* Body */} +
+ {apiError && ( + setApiError("")} /> + )} + + {/* Label + isPrimary */} +
+ + + +
+ + {/* Street */} + + + {/* City + State */} +
+ + +
+ + {/* Postal code + Country */} +
+ + +
+ + {/* Actions */} +
+ + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/Components/Client/Deleteconfirmmodal.tsx b/src/Components/Client/Deleteconfirmmodal.tsx new file mode 100644 index 0000000..20e805d --- /dev/null +++ b/src/Components/Client/Deleteconfirmmodal.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { useEffect } from "react"; +import { Spinner } from "../UI"; +import type { Client } from "@/src/types/client"; + +interface DeleteConfirmModalProps { + client: Client; + deleting: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +export function DeleteConfirmModal({ + client, + deleting, + onCancel, + onConfirm, +}: DeleteConfirmModalProps) { + // Close on Escape — identical to User/DeleteConfirmModal + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") onCancel(); + }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [onCancel]); + + return ( +
{ + if (e.target === e.currentTarget) onCancel(); + }} + style={{ + position: "fixed", + inset: 0, + zIndex: 60, + background: "rgba(15,23,42,0.55)", + backdropFilter: "blur(4px)", + display: "flex", + alignItems: "center", + justifyContent: "center", + padding: "1rem", + }} + > +
e.stopPropagation()} + style={{ + width: "100%", + maxWidth: 400, + background: "var(--color-surface)", + borderRadius: "var(--radius-xl)", + border: "1px solid #FECACA", + boxShadow: "0 20px 48px rgba(0,0,0,.18)", + padding: "2rem", + display: "flex", + flexDirection: "column", + gap: "1rem", + textAlign: "center", + }} + > + {/* أيقونة الحذف */} +
+ + + + + + +
+ +
+

+ حذف العميل +

+

+ هل أنت متأكد من حذف{" "} + + {client.name} + + ؟ سيتم حذف جميع عناوينه أيضاً. لا يمكن التراجع عن هذا الإجراء. +

+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/Components/Client/Toast.tsx b/src/Components/Client/Toast.tsx new file mode 100644 index 0000000..183241d --- /dev/null +++ b/src/Components/Client/Toast.tsx @@ -0,0 +1,5 @@ + + +// Re-export the canonical implementation so existing imports keep working. +export { Toast } from "@/src/Components/UI"; +export type { ToastNotification as Notification } from "@/src/Components/UI"; \ No newline at end of file diff --git a/src/Components/Client_Adress/AddressDetails.tsx b/src/Components/Client_Adress/AddressDetails.tsx new file mode 100644 index 0000000..db9f641 --- /dev/null +++ b/src/Components/Client_Adress/AddressDetails.tsx @@ -0,0 +1,258 @@ +"use client"; + +import type { ClientAddress } from "@/src/types/client_adresses"; + +// ─── Helpers ─────────────────────────────────────────────────────────────── + +function labelIcon(label: string): string { + const map: Record = { + "فوترة": "💳", + "شحن": "📦", + "المقر الرئيسي": "🏢", + "فرع": "🏬", + "مستودع": "🏭", + billing: "💳", + shipping: "📦", + "head office": "🏢", + branch: "🏬", + warehouse: "🏭", + }; + return map[label.toLowerCase()] ?? "📍"; +} + +// ─── Sub-components ──────────────────────────────────────────────────────── + +function DetailRow({ + label, + value, + ltr = false, +}: { + label: string; + value?: string | null; + ltr?: boolean; +}) { + if (!value) return null; + return ( +
+ + {label} + + + {value} + +
+ ); +} + +function SectionCard({ + title, + icon, + children, +}: { + title: string; + icon: string; + children: React.ReactNode; +}) { + return ( +
+ {/* Card header */} +
+ +

+ {title} +

+
+ + {/* Card body */} +
+ {children} +
+
+ ); +} + +// ─── Main component ──────────────────────────────────────────────────────── + +interface AddressDetailsProps { + address: ClientAddress; +} + +export function AddressDetails({ address }: AddressDetailsProps) { + const { details, contactPerson, location } = address; + + return ( +
+ {/* Identity card — label + branch */} +
+ + +
+

+ {address.label} +

+ {address.branchName && ( +

+ {address.branchName} +

+ )} +
+
+ + {/* Address details */} + + + + + + + + + + + + + + {/* Contact person — only when present */} + {(contactPerson?.name || contactPerson?.phone) && ( + + + + + )} + + {/* Coordinates */} + {location?.coordinates && ( + + + + + )} + + {/* System metadata */} + + + + +
+ ); +} \ No newline at end of file diff --git a/src/Components/Client_Adress/clientAdress.tsx b/src/Components/Client_Adress/clientAdress.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/Driver/DriverDeleteModal.tsx b/src/Components/Driver/DriverDeleteModal.tsx new file mode 100644 index 0000000..98b7d3c --- /dev/null +++ b/src/Components/Driver/DriverDeleteModal.tsx @@ -0,0 +1,100 @@ +"use client"; + + +import { useEffect } from "react"; +import { Spinner } from "../UI"; +import type { Driver } from "@/src/types/driver"; + +interface DriverDeleteModalProps { + driver: Driver; + deleting: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +export function DriverDeleteModal({ driver, deleting, onCancel, onConfirm }: DriverDeleteModalProps) { + useEffect(() => { + const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; + window.addEventListener("keydown", h); + return () => window.removeEventListener("keydown", h); + }, [onCancel]); + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + style={{ + position: "fixed", inset: 0, zIndex: 60, + background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", + display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", + }} + > +
e.stopPropagation()} + style={{ + width: "100%", maxWidth: 420, + background: "var(--color-surface)", + borderRadius: "var(--radius-xl)", + border: "1px solid #FECACA", + boxShadow: "0 20px 48px rgba(0,0,0,.18)", + padding: "2rem", + display: "flex", flexDirection: "column", gap: "1rem", + textAlign: "center", + }} + > + {/* Icon */} +
+ + + + + + +
+ +
+

+ حذف السائق +

+

+ هل أنت متأكد من حذف{" "} + {driver.name} + {" "}( + + {driver.phone} + + )؟ لا يمكن التراجع عن هذا الإجراء. +

+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/Components/Driver/DriverFormModalHelper.tsx b/src/Components/Driver/DriverFormModalHelper.tsx new file mode 100644 index 0000000..906ecb6 --- /dev/null +++ b/src/Components/Driver/DriverFormModalHelper.tsx @@ -0,0 +1,55 @@ +import React from "react"; + +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)", +}; + +export function FileInput({ + label, + current, + onChange, +}: { + label: string; + current: File | null; + onChange: (f: File | null) => void; +}) { + return ( + + ); +} diff --git a/src/Components/Driver/driver.tsx b/src/Components/Driver/driver.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/Home/home.tsx b/src/Components/Home/home.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/Order/OrderDeleteModal.tsx b/src/Components/Order/OrderDeleteModal.tsx new file mode 100644 index 0000000..e429dec --- /dev/null +++ b/src/Components/Order/OrderDeleteModal.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { useEffect } from "react"; +import { Spinner } from "../UI"; +import type { Order } from "@/src/types/order"; + +interface OrderDeleteModalProps { + order: Order; + deleting: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +/** + * Mirrors DriverDeleteModal.tsx 1:1 — same alertdialog role, same + * backdrop/click-outside-to-close behavior, same Escape-to-close effect, + * same button layout. Only the copy and the identifying fields differ. + */ +export function OrderDeleteModal({ order, deleting, onCancel, onConfirm }: OrderDeleteModalProps) { + useEffect(() => { + const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; + window.addEventListener("keydown", h); + return () => window.removeEventListener("keydown", h); + }, [onCancel]); + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + style={{ + position: "fixed", inset: 0, zIndex: 60, + background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", + display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", + }} + > +
e.stopPropagation()} + style={{ + width: "100%", maxWidth: 420, + background: "var(--color-surface)", + borderRadius: "var(--radius-xl)", + border: "1px solid #FECACA", + boxShadow: "0 20px 48px rgba(0,0,0,.18)", + padding: "2rem", + display: "flex", flexDirection: "column", gap: "1rem", + textAlign: "center", + }} + > + {/* Icon */} +
+ + + + + + +
+ +
+

+ حذف الطلب +

+

+ هل أنت متأكد من حذف الطلب{" "} + {order.shipmentNumber} + {" "}الخاص بـ{" "} + + {order.recipientName} + + ؟ لا يمكن التراجع عن هذا الإجراء. +

+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/Components/Order/OrderDetailBanel.tsx b/src/Components/Order/OrderDetailBanel.tsx new file mode 100644 index 0000000..fe4cd14 --- /dev/null +++ b/src/Components/Order/OrderDetailBanel.tsx @@ -0,0 +1,551 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { Spinner, Alert } from "../UI"; +import { orderService } from "@/src/services/order.service"; +import type { Order, OrderStatus, UpdateOrderStatusPayload } from "@/src/types/order"; + +const ORDER_STATUS_MAP: Record = { + Created: { label: "تم الإنشاء", color: "#1E40AF", bg: "#EFF6FF", border: "#BFDBFE", dot: "#3B82F6" }, + Assigned: { label: "مُعيَّن", color: "#5B21B6", bg: "#F5F3FF", border: "#DDD6FE", dot: "#8B5CF6" }, + InTransit: { label: "قيد التوصيل", color: "#854D0E", bg: "#FFFBEB", border: "#FDE68A", dot: "#D97706" }, + Delivered: { label: "تم التسليم", color: "#166534", bg: "#DCFCE7", border: "#BBF7D0", dot: "#16A34A" }, + Returned: { label: "مُرتجع", color: "#991B1B", bg: "#FEF2F2", border: "#FECACA", dot: "#DC2626" }, + Cancelled: { label: "ملغي", color: "#64748B", bg: "#F1F5F9", border: "#E2E8F0", dot: "#94A3B8" }, +}; + +const PAY_STATUS_MAP: Record = { + Pending: { label: "معلَّق", color: "#D97706" }, + Paid: { label: "مدفوع", color: "#16A34A" }, + Failed: { label: "فشل", color: "#DC2626" }, + Refunded: { label: "مُسترجع", color: "#64748B" }, +}; + +const PAY_METHOD_LABEL: Record = { + Cash: "نقداً", + Card: "بطاقة", + Prepaid: "مدفوع مسبقاً", +}; + +function fmtDate(iso?: string | null): string { + if (!iso) return "—"; + return new Date(iso).toLocaleDateString("ar-SA", { + year: "numeric", + month: "long", + day: "numeric", + }); +} + +function fmtAmount(n?: number | string | null): string { + if (n == null || n === "") return "—"; + const num = Number(n); + if (isNaN(num)) return "—"; + return `${num.toFixed(2)} ر.س`; +} + +function DetailRow({ + label, + value, + mono = false, + warn = false, +}: { + label: string; + value: string; + mono?: boolean; + warn?: boolean; +}) { + return ( +
+ + {label} + + + {warn && value !== "—" ? "⚠ " : ""} + {value} + +
+ ); +} + +function SectionHeading({ title }: { title: string }) { + return ( +

+ {title} +

+ ); +} + +interface OrderDetailPanelProps { + orderId: string; + onClose: () => void; + onEdit: (order: Order) => void; + onDelete: (order: Order) => void; + onStatusChanged?: (order: Order) => void; +} + +export function OrderDetailPanel({ + orderId, + onClose, + onEdit, + onDelete, + onStatusChanged, +}: OrderDetailPanelProps) { + const [order, setOrder] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [statusDraft, setStatusDraft] = useState(""); + const [statusReason, setStatusReason] = useState(""); + const [updatingStatus, setUpdatingStatus] = useState(false); + const [statusError, setStatusError] = useState(null); + + const loadOrder = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data = await orderService.getById(orderId); + setOrder(data); + setStatusDraft(data.currentStatus); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "تعذّر تحميل بيانات الطلب."); + } finally { + setLoading(false); + } + }, [orderId]); + + useEffect(() => { queueMicrotask(loadOrder); }, [loadOrder]); + + useEffect(() => { + const h = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; + window.addEventListener("keydown", h); + return () => window.removeEventListener("keydown", h); + }, [onClose]); + + const handleStatusUpdate = useCallback(async () => { + if (!order || !statusDraft || statusDraft === order.currentStatus) return; + setUpdatingStatus(true); + setStatusError(null); + try { + const payload: UpdateOrderStatusPayload = { status: statusDraft }; + if (statusReason) payload.reason = statusReason; + const updated = await orderService.updateStatus(order.id, payload); + setOrder(updated); + setStatusReason(""); + onStatusChanged?.(updated); + } catch (err: unknown) { + setStatusError(err instanceof Error ? err.message : "تعذّر تحديث حالة الطلب."); + } finally { + setUpdatingStatus(false); + } + }, [order, statusDraft, statusReason, onStatusChanged]); + + const statusConfig = order ? ORDER_STATUS_MAP[order.currentStatus] : null; + const payStatus = order?.paymentStatus ? PAY_STATUS_MAP[order.paymentStatus] : null; + + return ( + <> +
+ + + + ); +} + +export { ORDER_STATUS_MAP, PAY_STATUS_MAP, PAY_METHOD_LABEL }; \ No newline at end of file diff --git a/src/Components/Order/order.tsx b/src/Components/Order/order.tsx new file mode 100644 index 0000000..ee556ab --- /dev/null +++ b/src/Components/Order/order.tsx @@ -0,0 +1,210 @@ +"use client"; +import React, { type FC } from "react"; +import { useClientOrders } from "@/src/hooks/useClientOrders"; +import { fmtDate, fmtAmount } from "@/src/lib/formatters"; +import { statusColor, statusLabel } from "@/src/lib/order-status"; +import { clearSession, type ClientSession } from "@/src/lib/session"; +import type { OrderStatus } from "@/src/types/order"; +import { Spinner } from "../UI"; + +// ─── Order status tracker ───────────────────── +const STATUS_STEPS: OrderStatus[] = ["CREATED", "IN_TRANSIT", "DELIVERED"]; +const STEP_LABELS = ["تم الإنشاء", "قيد التوصيل", "تم التسليم"]; + +const StepIcon: FC<{ step: number }> = ({ step }) => { + if (step === 0) return ( + + + + + ); + if (step === 1) return ( + + + + + + ); + return ( + + + + ); +}; + +const OrderTracker: FC<{ status: OrderStatus }> = ({ status }) => { + const idx = STATUS_STEPS.indexOf(status === "CANCELLED" ? "CREATED" : status); + return ( +
+ {STATUS_STEPS.map((s, i) => ( + +
+
+ +
+ + {STEP_LABELS[i]} + +
+ {i < STATUS_STEPS.length - 1 && ( + + ); +}; + +// ─── DashboardStep ─────────────────────────── +interface DashboardStepProps { + session: ClientSession; +} + +const DashboardStep: FC = ({ session }) => { + const { orders, loading, error } = useClientOrders(session.id); + + const activeOrder = orders.find(o => o.status === "CREATED" || o.status === "IN_TRANSIT"); + const pastOrders = orders.filter(o => o !== activeOrder); + + const summary = { + total: orders.length, + delivered: orders.filter(o => o.status === "DELIVERED").length, + pending: orders.filter(o => o.status === "CREATED" || o.status === "IN_TRANSIT").length, + }; + + return ( +
+ {/* Header */} +
+
+

لوحة الطلبات

+

+ مرحبًا بعودتك، {session.name} +

+
+
+
+
+ + {/* Loading */} + {loading && ( +
+ +

جارٍ تحميل الطلبات…

+
+ )} + + {/* Error */} + {error && ( +
+

{error}

+
+ )} + + {!loading && !error && ( + <> + {/* Stats */} +
+ {[ + { label: "إجمالي الطلبات", value: summary.total, color: "text-[#111827]" }, + { label: "مُسلَّمة", value: summary.delivered, color: "text-[#34A853]" }, + { label: "قيد التنفيذ", value: summary.pending, color: "text-[#1A73E8]" }, + ].map(s => ( +
+

{s.label}

+

{s.value}

+
+ ))} +
+ + {/* Active order tracker */} + {activeOrder ? ( +
+
+
+

الطلب الحالي

+

{activeOrder.id}

+
+ + {statusLabel(activeOrder.status)} + +
+ +
+ ) : ( +
+

لا يوجد طلب نشط حاليًا.

+
+ )} + + {/* Past orders table */} + {pastOrders.length > 0 && ( +
+
+

سجل الطلبات

+ {pastOrders.length} طلبات +
+
+ + + + {["رقم الطلب", "التاريخ", "الوصف", "الحالة", "المبلغ"].map(h => ( + + ))} + + + + {pastOrders.map((order, i) => ( + + + + + + + + ))} + +
+ {h} +
+ {order.id} + {fmtDate(order.createdAt)}{order.description} + + + + {fmtAmount(order.totalAmount)} +
+
+
+ )} + + )} + + +
+ ); +}; + +export default DashboardStep; \ No newline at end of file diff --git a/src/Components/Trip/Tripdeletemodal.tsx b/src/Components/Trip/Tripdeletemodal.tsx new file mode 100644 index 0000000..193a5e9 --- /dev/null +++ b/src/Components/Trip/Tripdeletemodal.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { useEffect } from "react"; +import { Spinner } from "../UI"; +import type { Trip } from "@/src/types/trip"; + +interface TripDeleteModalProps { + trip: Trip; + deleting: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +export function TripDeleteModal({ trip, deleting, onCancel, onConfirm }: TripDeleteModalProps) { + useEffect(() => { + const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; + window.addEventListener("keydown", h); + return () => window.removeEventListener("keydown", h); + }, [onCancel]); + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + style={{ + position: "fixed", inset: 0, zIndex: 60, + background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", + display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", + }} + > +
e.stopPropagation()} + style={{ + width: "100%", maxWidth: 420, + background: "var(--color-surface)", + borderRadius: "var(--radius-xl)", + border: "1px solid #FECACA", + boxShadow: "0 20px 48px rgba(0,0,0,.18)", + padding: "2rem", + display: "flex", flexDirection: "column", gap: "1rem", + textAlign: "center", + }} + > + {/* Icon */} +
+ + + + + + +
+ +
+

+ حذف الرحلة +

+

+ هل أنت متأكد من حذف رحلة{" "} + {trip.title} + {" "}( + + {trip.tripNumber} + + )؟ لا يمكن التراجع عن هذا الإجراء. +

+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/Components/Trip/archive/ArchivedTripList.tsx b/src/Components/Trip/archive/ArchivedTripList.tsx new file mode 100644 index 0000000..9f7a862 --- /dev/null +++ b/src/Components/Trip/archive/ArchivedTripList.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { Spinner, Alert, EmptyState } from "@/src/Components/UI"; +import { useArchivedTrips } from "@/src/hooks/archive/useArchivedTrips"; +import { TRIP_STATUS_MAP } from "@/src/types/trip"; +import type { Trip } from "@/src/types/trip"; + +interface ArchivedTripListProps { + /** Called when the user clicks a row to view trip details */ + onView?: (trip: Trip) => void; +} + +// ── Status badge — reuses the existing trip status color map ──────────────── +function TripStatusBadge({ status }: { status: Trip["status"] }) { + const s = TRIP_STATUS_MAP[status]; + return ( + + + {s.label} + + ); +} + +// ── Date formatting helper ─────────────────────────────────────────────────── +function fmtDate(iso?: string | null): string { + if (!iso) return "—"; + return new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" }); +} + +/** + * ArchivedTripList + * Displays a paginated, searchable list of archived trips. + * Fetches data via useArchivedTrips (GET /trip/archived), handles loading, + * empty, and error states, and renders each trip with its key details. + * + * @example + * setViewTripId(trip.id)} /> + */ +export function ArchivedTripList({ onView }: ArchivedTripListProps) { + const { + trips, loading, total, pages, page, search, error, + setPage, handleSearch, clearError, + } = useArchivedTrips(); + + return ( +
+ {/* ── Header: count + search ── */} +
+

+ الرحلات المؤرشفة + + ({total}) + +

+ +
+ + + + handleSearch(e.target.value)} + className="h-10 w-full rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface)] pr-9 pl-3 text-[13px] text-[var(--color-text-primary)] outline-none focus:border-[var(--color-brand-600)] focus:ring-2 focus:ring-[var(--color-brand-600)]/15" + /> +
+
+ + {/* ── API error feedback ── */} + {error && } + + {/* ── Card ── */} +
+ {/* column headers */} +
+ الرحلة + الحالة + البدء + الانتهاء + النقد المحصّل +
+ + {/* ── Loading state ── */} + {loading ? ( +
+ + جارٍ التحميل… +
+ ) : trips.length === 0 ? ( + /* ── Empty state ── */ + + ) : ( + /* ── Data rows ── */ +
    + {trips.map((trip, i) => ( +
  • onView?.(trip)} + className={`grid grid-cols-[2fr_1.5fr_1fr_1fr_1fr] items-center gap-2 border-b border-[var(--color-border)] px-6 py-3.5 text-[13px] transition-colors hover:bg-[var(--color-surface-muted)] ${ + onView ? "cursor-pointer" : "" + } ${i % 2 !== 0 ? "bg-[var(--color-surface-muted)]/40" : ""}`} + > +
    +

    {trip.title}

    +

    {trip.tripNumber}

    +
    +
    + +
    + + {fmtDate(trip.startTime)} + + + {fmtDate(trip.endTime)} + + + {trip.totalCashCollected != null + ? `${Number(trip.totalCashCollected).toLocaleString("ar-SA")} ر.س` + : "—"} + +
  • + ))} +
+ )} + + {/* ── Pagination ── */} + {pages > 1 && ( +
+ + صفحة {page} من{" "} + {pages} + +
+ + +
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/Components/User/DeleteConfirmModal.tsx b/src/Components/User/DeleteConfirmModal.tsx new file mode 100644 index 0000000..c4b014d --- /dev/null +++ b/src/Components/User/DeleteConfirmModal.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { useEffect } from "react"; +import { Spinner } from "../UI"; +import type { User } from "@/src/types/user"; + +interface DeleteConfirmModalProps { + user: User; + deleting: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +export function DeleteConfirmModal({ user, deleting, onCancel, onConfirm }: DeleteConfirmModalProps) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [onCancel]); + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem" }} + > +
e.stopPropagation()} + style={{ width: "100%", maxWidth: 400, background: "var(--color-surface)", borderRadius: "var(--radius-xl)", border: "1px solid #FECACA", boxShadow: "0 20px 48px rgba(0,0,0,.18)", padding: "2rem", display: "flex", flexDirection: "column", gap: "1rem", textAlign: "center" }} + > + {/* icon delete */} +
+ + + + +
+
+

حذف المستخدم

+

+ هل أنت متأكد من حذف {user.name}؟ لا يمكن التراجع عن هذا الإجراء. +

+
+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/Components/User/Toast.tsx b/src/Components/User/Toast.tsx new file mode 100644 index 0000000..43bc0fc --- /dev/null +++ b/src/Components/User/Toast.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { Notification } from "@/src/hooks/useUser"; + +interface ToastProps { + notification: Notification | null; +} + +export function Toast({ notification }: ToastProps) { + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (notification) { + setVisible(true); + } else { + // dismiss after 250ms to allow exit animation + const t = setTimeout(() => setVisible(false), 300); + return () => clearTimeout(t); + } + }, [notification]); + + if (!visible && !notification) return null; + + const isSuccess = notification?.type === "success"; + + return ( +
+
+ {/* icon */} + {isSuccess ? "✓" : "⚠"} + {notification?.message} +
+
+ ); +} \ No newline at end of file diff --git a/src/Components/car/CarDeleteModal.tsx b/src/Components/car/CarDeleteModal.tsx new file mode 100644 index 0000000..8f7e475 --- /dev/null +++ b/src/Components/car/CarDeleteModal.tsx @@ -0,0 +1,86 @@ +"use client"; + + +import { useEffect } from "react"; +import { Spinner } from "../UI"; +import type { Car } from "@/src/types/car"; + +interface CarDeleteModalProps { + car: Car; + deleting: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +export function CarDeleteModal({ car, deleting, onCancel, onConfirm }: CarDeleteModalProps) { + useEffect(() => { + const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; + window.addEventListener("keydown", h); + return () => window.removeEventListener("keydown", h); + }, [onCancel]); + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + style={{ + position: "fixed", inset: 0, zIndex: 60, + background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", + display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", + }} + > +
e.stopPropagation()} + style={{ + width: "100%", maxWidth: 420, + background: "var(--color-surface)", + borderRadius: "var(--radius-xl)", + border: "1px solid #FECACA", + boxShadow: "0 20px 48px rgba(0,0,0,.18)", + padding: "2rem", + display: "flex", flexDirection: "column", gap: "1rem", + textAlign: "center", + }} + > + {/* Icon */} +
+ + + + + + +
+ +
+

+ حذف المركبة +

+

+ هل أنت متأكد من حذف{" "} + + {car.manufacturer} {car.model} + + {" "}( + + {car.plateLetters} {car.plateNumber} + + )؟ لا يمكن التراجع عن هذا الإجراء. +

+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/Components/role/DeleteRoleModal.tsx b/src/Components/role/DeleteRoleModal.tsx new file mode 100644 index 0000000..322b8cd --- /dev/null +++ b/src/Components/role/DeleteRoleModal.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useEffect } from "react"; +import { Spinner } from "../UI"; +import { Role } from "@/src/types/role"; + +interface DeleteRoleModalProps { + role: Role; + deleting: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +export function DeleteRoleModal({ role, deleting, onCancel, onConfirm }: DeleteRoleModalProps) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [onCancel]); + + return ( +
{ if (e.target === e.currentTarget) onCancel(); }} + style={{ + position: "fixed", inset: 0, zIndex: 60, + background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", + display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", + }} + > +
e.stopPropagation()} style={{ + width: "100%", maxWidth: 400, + background: "var(--color-surface)", + borderRadius: "var(--radius-xl)", + border: "1px solid #FECACA", + boxShadow: "0 20px 48px rgba(0,0,0,.18)", + padding: "2rem", + display: "flex", flexDirection: "column", gap: "1rem", textAlign: "center", + }}> + {/* Icon */} +
+ + + + + + +
+ +
+

+ حذف الدور +

+

+ هل أنت متأكد من حذف دور {role.name}؟ + لا يمكن التراجع عن هذا الإجراء. +

+
+ +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/hooks/useClientOrders.ts b/src/hooks/useClientOrders.ts new file mode 100644 index 0000000..088a092 --- /dev/null +++ b/src/hooks/useClientOrders.ts @@ -0,0 +1,45 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { clientService } from "@/src/services/client.service"; +import type { Order } from "@/src/types/order"; + +interface State { + orders: Order[]; + error: string | null; + loading: boolean; +} + +/** + * Fetches all orders for a client session. + * + * @example + * const { orders, loading, error } = useClientOrders(session.id); + */ +export function useClientOrders(clientId: string): State { + const [state, setState] = useState({ + orders: [], error: null, loading: true, + }); + + useEffect(() => { + if (!clientId) { + setState({ orders: [], error: null, loading: false }); + return; + } + + let cancelled = false; + + clientService + .getOrders(clientId) + .then(orders => { + if (!cancelled) setState({ orders, error: null, loading: false }); + }) + .catch((err: Error) => { + if (!cancelled) setState({ orders: [], error: err.message, loading: false }); + }); + + return () => { cancelled = true; }; + }, [clientId]); + + return state; +} \ No newline at end of file