The trip files have been reorganized. Errors related to vehicle and driver editing have been resolved, archive display issues fixed, and model data display standardized across the project; order and trip-related issues have also been addressed.
This commit is contained in:
@@ -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<ClientAddress | null>(null);
|
||||||
|
const [client, setClient] = useState<Client | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// ── Modal state ──────────────────────────────────────────────────────────
|
||||||
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
|
// ── Toast ────────────────────────────────────────────────────────────────
|
||||||
|
const [notification, setNotification] = useState<ToastNotification | null>(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<boolean> => {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
minHeight: "60vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: "3px solid var(--color-border)",
|
||||||
|
borderTopColor: "var(--color-brand-600)",
|
||||||
|
animation: "spin 0.7s linear infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !address) {
|
||||||
|
return (
|
||||||
|
<section style={{ padding: "2rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
|
<Alert type="error" message={error ?? "لم يتم العثور على العنوان."} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
style={{
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--color-brand-600)",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← رجوع
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Render ────────────────────────────────────────────────────────────────
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Toast notification={notification} />
|
||||||
|
|
||||||
|
{/* Edit modal */}
|
||||||
|
{editOpen && (
|
||||||
|
<AddressFormModal
|
||||||
|
editAddress={address}
|
||||||
|
onClose={() => setEditOpen(false)}
|
||||||
|
onSubmit={handleUpdateSubmit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete confirmation */}
|
||||||
|
<ConfirmDialog
|
||||||
|
open={deleteOpen}
|
||||||
|
loading={deleting}
|
||||||
|
onCancel={() => { if (!deleting) setDeleteOpen(false); }}
|
||||||
|
onConfirm={handleDeleteConfirm}
|
||||||
|
title="حذف العميل"
|
||||||
|
description={`هل أنت متأكد من حذف ${address.label}؟ سيتم حذف جميع عناوينه أيضاً. لا يمكن التراجع عن هذا الإجراء.`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
||||||
|
|
||||||
|
{/* ── Page header ── */}
|
||||||
|
<header
|
||||||
|
style={{
|
||||||
|
borderRadius: "var(--radius-xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
padding: "1.5rem 2rem",
|
||||||
|
boxShadow: "var(--shadow-card)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Back link */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.push(`/dashboard/clients/${clientId}/addresses`)}
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--color-text-muted)",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
marginBottom: 12,
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M15 18l-6-6 6-6" />
|
||||||
|
</svg>
|
||||||
|
{client ? `${client.name} — العناوين` : "العناوين"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
letterSpacing: "0.3em",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color: "#2563EB",
|
||||||
|
fontWeight: 600,
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
تفاصيل العنوان
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-3 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h1
|
||||||
|
style={{
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{address.label}
|
||||||
|
</h1>
|
||||||
|
{client && (
|
||||||
|
<p style={{ marginTop: "0.25rem", fontSize: 13, color: "var(--color-text-muted)" }}>
|
||||||
|
{client.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEditOpen(true)}
|
||||||
|
style={{
|
||||||
|
height: 40,
|
||||||
|
padding: "0 1rem",
|
||||||
|
borderRadius: "var(--radius-lg)",
|
||||||
|
border: "1px solid #BFDBFE",
|
||||||
|
background: "#EFF6FF",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#1D4ED8",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 7,
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
|
</svg>
|
||||||
|
تعديل العنوان
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setDeleteOpen(true)}
|
||||||
|
style={{
|
||||||
|
height: 40,
|
||||||
|
padding: "0 1rem",
|
||||||
|
borderRadius: "var(--radius-lg)",
|
||||||
|
border: "1px solid #FECACA",
|
||||||
|
background: "#FEF2F2",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#DC2626",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 7,
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6l-1 14H6L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" />
|
||||||
|
<path d="M9 6V4h6v2" />
|
||||||
|
</svg>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* ── Address details component ── */}
|
||||||
|
<AddressDetails address={address} />
|
||||||
|
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
app/dashboard/role_permission/page.tsx
Normal file
7
app/dashboard/role_permission/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default function page() {
|
||||||
|
return (
|
||||||
|
<div>page</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
0
src/Components/Admen/admen.tsx
Normal file
0
src/Components/Admen/admen.tsx
Normal file
58
src/Components/Branch/DeleteConfirmModal.tsx
Normal file
58
src/Components/Branch/DeleteConfirmModal.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="alertdialog" aria-modal="true" aria-labelledby="branch-del-title"
|
||||||
|
onClick={e => { 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" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => 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 */}
|
||||||
|
<div style={{ width: 52, height: 52, margin: "0 auto", borderRadius: "50%", background: "#FEF2F2", border: "1px solid #FECACA", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#DC2626" strokeWidth="2">
|
||||||
|
<polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" /><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 id="branch-del-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>حذف الفرع</h2>
|
||||||
|
<p style={{ marginTop: 8, fontSize: 13, color: "var(--color-text-muted)", lineHeight: 1.6 }}>
|
||||||
|
هل أنت متأكد من حذف فرع <strong style={{ color: "var(--color-text-primary)" }}>{branch.name}</strong>؟ لا يمكن التراجع عن هذا الإجراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button type="button" onClick={onCancel} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)", cursor: deleting ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)" }}>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "none", background: "#DC2626", fontSize: 13, fontWeight: 700, color: "#FFF", cursor: deleting ? "not-allowed" : "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 8, fontFamily: "var(--font-sans)", opacity: deleting ? 0.7 : 1 }}>
|
||||||
|
{deleting && <Spinner size="sm" className="text-white" />}
|
||||||
|
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
91
src/Components/Car_Maintanance/CarMaintananceDeleteModal.tsx
Normal file
91
src/Components/Car_Maintanance/CarMaintananceDeleteModal.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="alertdialog" aria-modal="true" aria-labelledby="maintenance-del-title"
|
||||||
|
onClick={e => { 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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => 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 */}
|
||||||
|
<div style={{ width: 52, height: 52, margin: "0 auto", borderRadius: "50%", background: "#FEF2F2", border: "1px solid #FECACA", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#DC2626" strokeWidth="2">
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" />
|
||||||
|
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 id="maintenance-del-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
|
||||||
|
حذف سجل الصيانة
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginTop: 8, fontSize: 13, color: "var(--color-text-muted)", lineHeight: 1.6 }}>
|
||||||
|
هل أنت متأكد من حذف سجل{" "}
|
||||||
|
<strong style={{ color: "var(--color-text-primary)" }}>
|
||||||
|
{record.reason}
|
||||||
|
</strong>
|
||||||
|
{" "}(
|
||||||
|
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12 }}>
|
||||||
|
{fmtCost(record.cost)}
|
||||||
|
</span>
|
||||||
|
)؟ لا يمكن التراجع عن هذا الإجراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button type="button" onClick={onCancel} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)", cursor: deleting ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)" }}>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "none", background: "#DC2626", fontSize: 13, fontWeight: 700, color: "#FFF", cursor: deleting ? "not-allowed" : "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 8, fontFamily: "var(--font-sans)", opacity: deleting ? 0.7 : 1 }}>
|
||||||
|
{deleting && <Spinner size="sm" className="text-white" />}
|
||||||
|
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
392
src/Components/Client/Addressformmodal.tsx
Normal file
392
src/Components/Client/Addressformmodal.tsx
Normal file
@@ -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<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Component ──────────────────────────────────────────────────────────────
|
||||||
|
export function AddressFormModal({
|
||||||
|
editAddress,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
}: AddressFormModalProps) {
|
||||||
|
const isNew = editAddress === null;
|
||||||
|
|
||||||
|
const [form, setForm] = useState<ClientAddressFormData>({
|
||||||
|
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<ClientAddressFormErrors>({});
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [apiError, setApiError] = useState("");
|
||||||
|
const firstRef = useRef<HTMLSelectElement>(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<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="addr-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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => 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 */}
|
||||||
|
<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="addr-modal-title"
|
||||||
|
style={{
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
margin: "4px 0 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isNew ? "عنوان جديد" : editAddress?.label}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
noValidate
|
||||||
|
style={{
|
||||||
|
padding: "1.5rem",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{apiError && (
|
||||||
|
<Alert type="error" message={apiError} onClose={() => setApiError("")} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Label + isPrimary */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr auto",
|
||||||
|
gap: "0.75rem",
|
||||||
|
alignItems: "end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label style={S.label}>
|
||||||
|
نوع العنوان *
|
||||||
|
<select
|
||||||
|
ref={firstRef}
|
||||||
|
style={{ ...inputStyle("label"), cursor: "pointer" }}
|
||||||
|
value={form.label}
|
||||||
|
onChange={set("label")}
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<option value="">اختر النوع</option>
|
||||||
|
{LABEL_PRESETS.map((l) => (
|
||||||
|
<option key={l} value={l}>{l}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{errors.label && <span style={S.errorText}>{errors.label}</span>}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--color-text-secondary)",
|
||||||
|
paddingBottom: 2,
|
||||||
|
cursor: "pointer",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={form.isPrimary}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((p) => ({ ...p, isPrimary: e.target.checked }))
|
||||||
|
}
|
||||||
|
style={{ width: 14, height: 14, cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
عنوان رئيسي
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Street */}
|
||||||
|
<label style={S.label}>
|
||||||
|
الشارع / العنوان التفصيلي *
|
||||||
|
<input
|
||||||
|
style={inputStyle("street")}
|
||||||
|
value={form.street}
|
||||||
|
onChange={set("street")}
|
||||||
|
placeholder="شارع الملك فهد، مبنى 12"
|
||||||
|
dir="rtl"
|
||||||
|
/>
|
||||||
|
{errors.street && <span style={S.errorText}>{errors.street}</span>}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* City + State */}
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
|
||||||
|
<label style={S.label}>
|
||||||
|
المدينة *
|
||||||
|
<input
|
||||||
|
style={inputStyle("city")}
|
||||||
|
value={form.city}
|
||||||
|
onChange={set("city")}
|
||||||
|
placeholder="الرياض"
|
||||||
|
dir="rtl"
|
||||||
|
/>
|
||||||
|
{errors.city && <span style={S.errorText}>{errors.city}</span>}
|
||||||
|
</label>
|
||||||
|
<label style={S.label}>
|
||||||
|
المنطقة *
|
||||||
|
<input
|
||||||
|
style={inputStyle("state")}
|
||||||
|
value={form.state}
|
||||||
|
onChange={set("state")}
|
||||||
|
placeholder="منطقة الرياض"
|
||||||
|
dir="rtl"
|
||||||
|
/>
|
||||||
|
{errors.state && <span style={S.errorText}>{errors.state}</span>}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Postal code + Country */}
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
|
||||||
|
<label style={S.label}>
|
||||||
|
الرمز البريدي *
|
||||||
|
<input
|
||||||
|
style={inputStyle("postalCode")}
|
||||||
|
value={form.postalCode}
|
||||||
|
onChange={set("postalCode")}
|
||||||
|
placeholder="11564"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
{errors.postalCode && (
|
||||||
|
<span style={S.errorText}>{errors.postalCode}</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<label style={S.label}>
|
||||||
|
الدولة *
|
||||||
|
<input
|
||||||
|
style={inputStyle("country")}
|
||||||
|
value={form.country}
|
||||||
|
onChange={set("country")}
|
||||||
|
placeholder="المملكة العربية السعودية"
|
||||||
|
dir="rtl"
|
||||||
|
/>
|
||||||
|
{errors.country && <span style={S.errorText}>{errors.country}</span>}
|
||||||
|
</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}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
171
src/Components/Client/Deleteconfirmmodal.tsx
Normal file
171
src/Components/Client/Deleteconfirmmodal.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="alertdialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="del-client-title"
|
||||||
|
onClick={(e) => {
|
||||||
|
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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => 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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* أيقونة الحذف */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
margin: "0 auto",
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "#FEF2F2",
|
||||||
|
border: "1px solid #FECACA",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="22"
|
||||||
|
height="22"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="#DC2626"
|
||||||
|
strokeWidth="2"
|
||||||
|
>
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" />
|
||||||
|
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
id="del-client-title"
|
||||||
|
style={{
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
حذف العميل
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
marginTop: 8,
|
||||||
|
fontSize: 13,
|
||||||
|
color: "var(--color-text-muted)",
|
||||||
|
lineHeight: 1.6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
هل أنت متأكد من حذف{" "}
|
||||||
|
<strong style={{ color: "var(--color-text-primary)" }}>
|
||||||
|
{client.name}
|
||||||
|
</strong>
|
||||||
|
؟ سيتم حذف جميع عناوينه أيضاً. لا يمكن التراجع عن هذا الإجراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={deleting}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--color-text-secondary)",
|
||||||
|
cursor: deleting ? "not-allowed" : "pointer",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={deleting}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
border: "none",
|
||||||
|
background: "#DC2626",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "#FFF",
|
||||||
|
cursor: deleting ? "not-allowed" : "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 8,
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
opacity: deleting ? 0.7 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{deleting && <Spinner size="sm" className="text-white" />}
|
||||||
|
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/Components/Client/Toast.tsx
Normal file
5
src/Components/Client/Toast.tsx
Normal file
@@ -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";
|
||||||
258
src/Components/Client_Adress/AddressDetails.tsx
Normal file
258
src/Components/Client_Adress/AddressDetails.tsx
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { ClientAddress } from "@/src/types/client_adresses";
|
||||||
|
|
||||||
|
// ─── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function labelIcon(label: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
"فوترة": "💳",
|
||||||
|
"شحن": "📦",
|
||||||
|
"المقر الرئيسي": "🏢",
|
||||||
|
"فرع": "🏬",
|
||||||
|
"مستودع": "🏭",
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 4,
|
||||||
|
padding: "0.75rem 0",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "var(--color-text-muted)",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.07em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
dir={ltr ? "ltr" : "rtl"}
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
fontFamily: ltr ? "var(--font-mono)" : "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionCard({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: "var(--radius-xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
boxShadow: "var(--shadow-card)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Card header */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
padding: "0.875rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 16 }} aria-hidden="true">{icon}</span>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "var(--color-text-secondary)",
|
||||||
|
letterSpacing: "0.05em",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card body */}
|
||||||
|
<div
|
||||||
|
dir="rtl"
|
||||||
|
style={{ padding: "0 1.5rem" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main component ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface AddressDetailsProps {
|
||||||
|
address: ClientAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddressDetails({ address }: AddressDetailsProps) {
|
||||||
|
const { details, contactPerson, location } = address;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Identity card — label + branch */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderRadius: "var(--radius-xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
boxShadow: "var(--shadow-card)",
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "1rem",
|
||||||
|
}}
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
borderRadius: "var(--radius-lg)",
|
||||||
|
background: "var(--color-brand-50)",
|
||||||
|
border: "1px solid var(--color-brand-100)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: 24,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{labelIcon(address.label)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{address.label}
|
||||||
|
</p>
|
||||||
|
{address.branchName && (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
margin: "3px 0 0",
|
||||||
|
fontSize: 13,
|
||||||
|
color: "var(--color-text-muted)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{address.branchName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Address details */}
|
||||||
|
<SectionCard title="تفاصيل العنوان" icon="🗺️">
|
||||||
|
<DetailRow label="الشارع" value={details.street} />
|
||||||
|
<DetailRow label="المدينة" value={details.city} />
|
||||||
|
<DetailRow label="المنطقة / الولاية" value={details.state} />
|
||||||
|
<DetailRow label="الحي" value={details.district} />
|
||||||
|
<DetailRow label="رقم المبنى" value={details.buildingNo} ltr />
|
||||||
|
<DetailRow label="رقم الوحدة" value={details.unitNo} ltr />
|
||||||
|
<DetailRow label="الرقم الإضافي" value={details.additionalNo} ltr />
|
||||||
|
<DetailRow label="الرمز البريدي" value={details.zipCode} ltr />
|
||||||
|
<DetailRow label="الشقة / الطابق" value={details.apartment} />
|
||||||
|
<DetailRow label="الدولة" value={details.country} />
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
{/* Contact person — only when present */}
|
||||||
|
{(contactPerson?.name || contactPerson?.phone) && (
|
||||||
|
<SectionCard title="جهة الاتصال" icon="👤">
|
||||||
|
<DetailRow label="الاسم" value={contactPerson.name} />
|
||||||
|
<DetailRow label="رقم الهاتف" value={contactPerson.phone} ltr />
|
||||||
|
</SectionCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Coordinates */}
|
||||||
|
{location?.coordinates && (
|
||||||
|
<SectionCard title="الإحداثيات الجغرافية" icon="📡">
|
||||||
|
<DetailRow
|
||||||
|
label="خط الطول (Longitude)"
|
||||||
|
value={String(location.coordinates[0])}
|
||||||
|
ltr
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="خط العرض (Latitude)"
|
||||||
|
value={String(location.coordinates[1])}
|
||||||
|
ltr
|
||||||
|
/>
|
||||||
|
</SectionCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* System metadata */}
|
||||||
|
<SectionCard title="معلومات النظام" icon="🕐">
|
||||||
|
<DetailRow
|
||||||
|
label="تاريخ الإنشاء"
|
||||||
|
value={new Date(address.createdAt).toLocaleString("ar-SA", {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="آخر تحديث"
|
||||||
|
value={new Date(address.updatedAt).toLocaleString("ar-SA", {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "short",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</SectionCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/Components/Client_Adress/clientAdress.tsx
Normal file
0
src/Components/Client_Adress/clientAdress.tsx
Normal file
100
src/Components/Driver/DriverDeleteModal.tsx
Normal file
100
src/Components/Driver/DriverDeleteModal.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="alertdialog" aria-modal="true" aria-labelledby="driver-del-title"
|
||||||
|
onClick={e => { 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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => 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 */}
|
||||||
|
<div style={{
|
||||||
|
width: 52, height: 52, margin: "0 auto", borderRadius: "50%",
|
||||||
|
background: "#FEF2F2", border: "1px solid #FECACA",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
}}>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#DC2626" strokeWidth="2">
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" />
|
||||||
|
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 id="driver-del-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
|
||||||
|
حذف السائق
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginTop: 8, fontSize: 13, color: "var(--color-text-muted)", lineHeight: 1.6 }}>
|
||||||
|
هل أنت متأكد من حذف{" "}
|
||||||
|
<strong style={{ color: "var(--color-text-primary)" }}>{driver.name}</strong>
|
||||||
|
{" "}(
|
||||||
|
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12 }}>
|
||||||
|
{driver.phone}
|
||||||
|
</span>
|
||||||
|
)؟ لا يمكن التراجع عن هذا الإجراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button type="button" onClick={onCancel} disabled={deleting}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: 40, borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-border)", background: "var(--color-surface)",
|
||||||
|
fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)",
|
||||||
|
cursor: deleting ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)",
|
||||||
|
}}>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: 40, borderRadius: "var(--radius-md)",
|
||||||
|
border: "none", background: "#DC2626",
|
||||||
|
fontSize: 13, fontWeight: 700, color: "#FFF",
|
||||||
|
cursor: deleting ? "not-allowed" : "pointer",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center", gap: 8,
|
||||||
|
fontFamily: "var(--font-sans)", opacity: deleting ? 0.7 : 1,
|
||||||
|
}}>
|
||||||
|
{deleting && <Spinner size="sm" className="text-white" />}
|
||||||
|
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/Components/Driver/DriverFormModalHelper.tsx
Normal file
55
src/Components/Driver/DriverFormModalHelper.tsx
Normal file
@@ -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 (
|
||||||
|
<label style={labelStyle}>
|
||||||
|
{label}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) => onChange(e.target.files?.[0] ?? null)}
|
||||||
|
style={{
|
||||||
|
...inputBase,
|
||||||
|
padding: "0.35rem 0.75rem",
|
||||||
|
height: "auto",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{current && (
|
||||||
|
<span style={{ fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||||
|
{current.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
src/Components/Driver/driver.tsx
Normal file
0
src/Components/Driver/driver.tsx
Normal file
0
src/Components/Home/home.tsx
Normal file
0
src/Components/Home/home.tsx
Normal file
104
src/Components/Order/OrderDeleteModal.tsx
Normal file
104
src/Components/Order/OrderDeleteModal.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="alertdialog" aria-modal="true" aria-labelledby="order-del-title"
|
||||||
|
onClick={e => { 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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => 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 */}
|
||||||
|
<div style={{
|
||||||
|
width: 52, height: 52, margin: "0 auto", borderRadius: "50%",
|
||||||
|
background: "#FEF2F2", border: "1px solid #FECACA",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
}}>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#DC2626" strokeWidth="2">
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" />
|
||||||
|
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 id="order-del-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
|
||||||
|
حذف الطلب
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginTop: 8, fontSize: 13, color: "var(--color-text-muted)", lineHeight: 1.6 }}>
|
||||||
|
هل أنت متأكد من حذف الطلب{" "}
|
||||||
|
<strong style={{ color: "var(--color-text-primary)" }}>{order.shipmentNumber}</strong>
|
||||||
|
{" "}الخاص بـ{" "}
|
||||||
|
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12 }}>
|
||||||
|
{order.recipientName}
|
||||||
|
</span>
|
||||||
|
؟ لا يمكن التراجع عن هذا الإجراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button type="button" onClick={onCancel} disabled={deleting}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: 40, borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-border)", background: "var(--color-surface)",
|
||||||
|
fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)",
|
||||||
|
cursor: deleting ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)",
|
||||||
|
}}>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: 40, borderRadius: "var(--radius-md)",
|
||||||
|
border: "none", background: "#DC2626",
|
||||||
|
fontSize: 13, fontWeight: 700, color: "#FFF",
|
||||||
|
cursor: deleting ? "not-allowed" : "pointer",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center", gap: 8,
|
||||||
|
fontFamily: "var(--font-sans)", opacity: deleting ? 0.7 : 1,
|
||||||
|
}}>
|
||||||
|
{deleting && <Spinner size="sm" className="text-white" />}
|
||||||
|
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
551
src/Components/Order/OrderDetailBanel.tsx
Normal file
551
src/Components/Order/OrderDetailBanel.tsx
Normal file
@@ -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<OrderStatus, { label: string; color: string; bg: string; border: string; dot: string }> = {
|
||||||
|
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<string, { label: string; color: string }> = {
|
||||||
|
Pending: { label: "معلَّق", color: "#D97706" },
|
||||||
|
Paid: { label: "مدفوع", color: "#16A34A" },
|
||||||
|
Failed: { label: "فشل", color: "#DC2626" },
|
||||||
|
Refunded: { label: "مُسترجع", color: "#64748B" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const PAY_METHOD_LABEL: Record<string, string> = {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "baseline",
|
||||||
|
padding: "0.55rem 0",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--color-text-muted)", fontWeight: 600 }}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: mono ? "var(--font-mono)" : "var(--font-sans)",
|
||||||
|
color: warn ? "#D97706" : "var(--color-text-primary)",
|
||||||
|
fontWeight: warn ? 600 : 400,
|
||||||
|
maxWidth: "60%",
|
||||||
|
textAlign: "left",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{warn && value !== "—" ? "⚠ " : ""}
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionHeading({ title }: { title: string }) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: 11,
|
||||||
|
letterSpacing: "0.25em",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
color: "var(--color-text-hint)",
|
||||||
|
fontWeight: 700,
|
||||||
|
margin: "1.25rem 0 0.5rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Order | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [statusDraft, setStatusDraft] = useState<OrderStatus | "">("");
|
||||||
|
const [statusReason, setStatusReason] = useState("");
|
||||||
|
const [updatingStatus, setUpdatingStatus] = useState(false);
|
||||||
|
const [statusError, setStatusError] = useState<string | null>(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 (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
inset: 0,
|
||||||
|
zIndex: 40,
|
||||||
|
background: "rgba(15,23,42,0.45)",
|
||||||
|
backdropFilter: "blur(2px)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
aria-label="تفاصيل الطلب"
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 50,
|
||||||
|
width: "min(520px, 100vw)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
borderRight: "1px solid var(--color-border)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
boxShadow: "8px 0 40px rgba(0,0,0,.18)",
|
||||||
|
overflowY: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12 }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: "50%",
|
||||||
|
flexShrink: 0,
|
||||||
|
border: "2px solid var(--color-brand-200)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-brand-600)" strokeWidth="2">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2"/>
|
||||||
|
<path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600, margin: 0 }}>
|
||||||
|
ملف الطلب
|
||||||
|
</p>
|
||||||
|
{order && (
|
||||||
|
<h2 style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "3px 0 0", fontFamily: "var(--font-mono)" }}>
|
||||||
|
{order.shipmentNumber}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{order?.trip?.tripNumber && (
|
||||||
|
<p style={{ fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--color-text-muted)", margin: "2px 0 0" }}>
|
||||||
|
رحلة: {order.trip.tripNumber}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</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",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflowY: "auto", padding: "1.25rem 1.5rem" }}>
|
||||||
|
{loading && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 12, padding: "2rem 0" }}>
|
||||||
|
<Spinner size="sm" className="text-blue-600" />
|
||||||
|
<span style={{ fontSize: 13, color: "var(--color-text-muted)" }}>جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div style={{
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
background: "#FEF2F2",
|
||||||
|
border: "1px solid #FECACA",
|
||||||
|
padding: "0.75rem 1rem",
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#DC2626",
|
||||||
|
}}>
|
||||||
|
⚠ {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{order && !loading && (
|
||||||
|
<>
|
||||||
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: "1rem" }}>
|
||||||
|
{statusConfig && (
|
||||||
|
<span style={{
|
||||||
|
borderRadius: "var(--radius-full)",
|
||||||
|
border: `1px solid ${statusConfig.border}`,
|
||||||
|
background: statusConfig.bg,
|
||||||
|
padding: "0.3rem 0.875rem",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: statusConfig.color,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 6,
|
||||||
|
}}>
|
||||||
|
<span style={{ width: 7, height: 7, borderRadius: "50%", background: statusConfig.dot }} />
|
||||||
|
{statusConfig.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{payStatus && (
|
||||||
|
<span style={{
|
||||||
|
borderRadius: "var(--radius-full)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
padding: "0.3rem 0.875rem",
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: payStatus.color,
|
||||||
|
}}>
|
||||||
|
{payStatus.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SectionHeading title="بيانات المستلم" />
|
||||||
|
<DetailRow label="اسم المستلم" value={order.recipientName} />
|
||||||
|
<DetailRow label="رقم الجوال" value={order.recipientPhone} mono />
|
||||||
|
<DetailRow label="العميل" value={order.client?.name ?? "—"} />
|
||||||
|
|
||||||
|
<SectionHeading title="بيانات الشحنة" />
|
||||||
|
<DetailRow label="رقم الشحنة" value={order.shipmentNumber} mono />
|
||||||
|
<DetailRow label="نوع الشحنة" value={order.type ?? "—"} />
|
||||||
|
<DetailRow label="الكمية" value={String(order.quantity ?? "—")} />
|
||||||
|
<DetailRow label="الوزن" value={order.weight != null ? `${order.weight} كجم` : "—"} />
|
||||||
|
<DetailRow label="الرحلة" value={order.trip?.tripNumber ?? "—"} mono />
|
||||||
|
|
||||||
|
<SectionHeading title="بيانات الدفع" />
|
||||||
|
<DetailRow label="طريقة الدفع" value={order.paymentMethod ? PAY_METHOD_LABEL[order.paymentMethod] ?? order.paymentMethod : "—"} />
|
||||||
|
<DetailRow label="حالة الدفع" value={payStatus?.label ?? "—"} />
|
||||||
|
<DetailRow label="الإجمالي الفرعي" value={fmtAmount(order.subTotal)} mono />
|
||||||
|
<DetailRow label="ضريبة القيمة المضافة" value={fmtAmount(order.vatAmount)} mono />
|
||||||
|
<DetailRow label="الإجمالي" value={fmtAmount(order.totalPrice)} mono />
|
||||||
|
|
||||||
|
{order.deliveryAddress?.details && Object.values(order.deliveryAddress.details).some(Boolean) && (
|
||||||
|
<>
|
||||||
|
<SectionHeading title="عنوان التسليم" />
|
||||||
|
{order.deliveryAddress.details.city && <DetailRow label="المدينة" value={order.deliveryAddress.details.city} />}
|
||||||
|
{order.deliveryAddress.details.district && <DetailRow label="الحي" value={order.deliveryAddress.details.district} />}
|
||||||
|
{order.deliveryAddress.details.street && <DetailRow label="الشارع" value={order.deliveryAddress.details.street} />}
|
||||||
|
{order.deliveryAddress.details.buildingNo && <DetailRow label="رقم المبنى" value={order.deliveryAddress.details.buildingNo} mono />}
|
||||||
|
{order.deliveryAddress.details.unitNo && <DetailRow label="رقم الوحدة" value={order.deliveryAddress.details.unitNo} mono />}
|
||||||
|
{order.deliveryAddress.details.zipCode && <DetailRow label="الرمز البريدي" value={order.deliveryAddress.details.zipCode} mono />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!order.deliveryAddress?.details && order.deliveryAddressId && (
|
||||||
|
<>
|
||||||
|
<SectionHeading title="عنوان التسليم" />
|
||||||
|
<DetailRow label="معرّف العنوان" value={order.deliveryAddressId} mono />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{order.pickupAddress?.details && Object.values(order.pickupAddress.details).some(Boolean) && (
|
||||||
|
<>
|
||||||
|
<SectionHeading title="عنوان الاستلام" />
|
||||||
|
{order.pickupAddress.details.city && <DetailRow label="المدينة" value={order.pickupAddress.details.city} />}
|
||||||
|
{order.pickupAddress.details.district && <DetailRow label="الحي" value={order.pickupAddress.details.district} />}
|
||||||
|
{order.pickupAddress.details.street && <DetailRow label="الشارع" value={order.pickupAddress.details.street} />}
|
||||||
|
{order.pickupAddress.details.buildingNo && <DetailRow label="رقم المبنى" value={order.pickupAddress.details.buildingNo} mono />}
|
||||||
|
{order.pickupAddress.details.unitNo && <DetailRow label="رقم الوحدة" value={order.pickupAddress.details.unitNo} mono />}
|
||||||
|
{order.pickupAddress.details.zipCode && <DetailRow label="الرمز البريدي" value={order.pickupAddress.details.zipCode} mono />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!order.pickupAddress?.details && order.pickupAddressId && (
|
||||||
|
<>
|
||||||
|
<SectionHeading title="عنوان الاستلام" />
|
||||||
|
<DetailRow label="معرّف العنوان" value={order.pickupAddressId} mono />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SectionHeading title="معلومات النظام" /> <DetailRow label="تاريخ الإنشاء" value={fmtDate(order.createdAt)} />
|
||||||
|
<DetailRow label="آخر تحديث" value={fmtDate(order.updatedAt)} />
|
||||||
|
|
||||||
|
<SectionHeading title="تحديث الحالة" />
|
||||||
|
{statusError && (
|
||||||
|
<Alert type="error" message={statusError} onClose={() => setStatusError(null)} />
|
||||||
|
)}
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 8 }}>
|
||||||
|
<select
|
||||||
|
value={statusDraft}
|
||||||
|
onChange={(e) => setStatusDraft(e.target.value as OrderStatus)}
|
||||||
|
dir="rtl"
|
||||||
|
style={{
|
||||||
|
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)",
|
||||||
|
fontFamily: "var(--font-sans)", cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(ORDER_STATUS_MAP).map(([k, v]) => (
|
||||||
|
<option key={k} value={k}>{v.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={statusReason}
|
||||||
|
onChange={(e) => setStatusReason(e.target.value)}
|
||||||
|
placeholder="سبب التغيير (اختياري)"
|
||||||
|
dir="rtl"
|
||||||
|
style={{
|
||||||
|
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)",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleStatusUpdate}
|
||||||
|
disabled={updatingStatus || !statusDraft || statusDraft === order.currentStatus}
|
||||||
|
style={{
|
||||||
|
height: 40,
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
border: "none",
|
||||||
|
background: updatingStatus ? "var(--color-brand-400)" : "var(--color-brand-600)",
|
||||||
|
fontSize: 13, fontWeight: 700, color: "#FFF",
|
||||||
|
cursor: (updatingStatus || !statusDraft || statusDraft === order.currentStatus) ? "not-allowed" : "pointer",
|
||||||
|
opacity: (!statusDraft || statusDraft === order.currentStatus) ? 0.6 : 1,
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center", gap: 8,
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{updatingStatus && <Spinner size="sm" className="text-white" />}
|
||||||
|
{updatingStatus ? "جارٍ التحديث…" : "تحديث الحالة"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{order.statusHistory && order.statusHistory.length > 0 && (
|
||||||
|
<>
|
||||||
|
<SectionHeading title="سجل الحالات" />
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||||
|
{order.statusHistory.slice(0, 5).map((h) => {
|
||||||
|
const s = ORDER_STATUS_MAP[h.status] ?? ORDER_STATUS_MAP.Created;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={h.id}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
border: `1px solid ${s.border}`,
|
||||||
|
background: s.bg,
|
||||||
|
padding: "0.5rem 0.875rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 600, color: s.color }}>{s.label}</span>
|
||||||
|
{h.reason && (
|
||||||
|
<span style={{ fontSize: 11, color: "var(--color-text-muted)", marginRight: 8 }}>
|
||||||
|
— {h.reason}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||||
|
{fmtDate(h.createdAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{order && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "1rem 1.5rem",
|
||||||
|
borderTop: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.75rem",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onDelete(order)}
|
||||||
|
style={{
|
||||||
|
height: 40, padding: "0 1rem",
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid #FECACA",
|
||||||
|
background: "#FEF2F2",
|
||||||
|
fontSize: 13, fontWeight: 700, color: "#DC2626",
|
||||||
|
cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onEdit(order)}
|
||||||
|
style={{
|
||||||
|
height: 40, padding: "0 1rem",
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-brand-200)",
|
||||||
|
background: "var(--color-brand-50, #EFF6FF)",
|
||||||
|
fontSize: 13, fontWeight: 700, color: "var(--color-brand-600)",
|
||||||
|
cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||||
|
display: "flex", alignItems: "center", gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||||
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||||
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||||
|
</svg>
|
||||||
|
تعديل
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: 40,
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)",
|
||||||
|
cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
إغلاق
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ORDER_STATUS_MAP, PAY_STATUS_MAP, PAY_METHOD_LABEL };
|
||||||
210
src/Components/Order/order.tsx
Normal file
210
src/Components/Order/order.tsx
Normal file
@@ -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 (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<rect x="9" y="3" width="6" height="4" rx="1"/>
|
||||||
|
<path d="M4 7h16v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
if (step === 1) return (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2"/>
|
||||||
|
<path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/>
|
||||||
|
<line x1="12" y1="12" x2="12" y2="16"/><line x1="10" y1="14" x2="14" y2="14"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderTracker: FC<{ status: OrderStatus }> = ({ status }) => {
|
||||||
|
const idx = STATUS_STEPS.indexOf(status === "CANCELLED" ? "CREATED" : status);
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-0 py-2" dir="rtl" role="list" aria-label="تقدم الطلب">
|
||||||
|
{STATUS_STEPS.map((s, i) => (
|
||||||
|
<React.Fragment key={s}>
|
||||||
|
<div className="flex flex-col items-center gap-1" role="listitem">
|
||||||
|
<div className={`w-9 h-9 rounded-full flex items-center justify-center transition-all ${
|
||||||
|
status === "CANCELLED"
|
||||||
|
? "bg-[#FEE2E2] text-red-500 border-2 border-red-200"
|
||||||
|
: i <= idx ? "bg-[#1A73E8] text-white" : "bg-white border-2 border-[#E5E7EB] text-[#D1D5DB]"
|
||||||
|
}`} aria-current={i === idx ? "step" : undefined}>
|
||||||
|
<StepIcon step={i} />
|
||||||
|
</div>
|
||||||
|
<span className={`text-[10px] font-semibold whitespace-nowrap ${
|
||||||
|
status === "CANCELLED" ? "text-red-400" : i <= idx ? "text-[#1A73E8]" : "text-[#9CA3AF]"
|
||||||
|
}`}>
|
||||||
|
{STEP_LABELS[i]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{i < STATUS_STEPS.length - 1 && (
|
||||||
|
<div aria-hidden="true" className={`flex-1 h-0.5 mb-4 mx-1 ${
|
||||||
|
status === "CANCELLED" ? "bg-red-200" : i < idx ? "bg-[#1A73E8]" : "bg-[#E5E7EB]"
|
||||||
|
}`} />
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── DashboardStep ───────────────────────────
|
||||||
|
interface DashboardStepProps {
|
||||||
|
session: ClientSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardStep: FC<DashboardStepProps> = ({ 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 (
|
||||||
|
<div dir="rtl">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-[22px] font-bold text-[#111827] tracking-tight">لوحة الطلبات</h2>
|
||||||
|
<p className="text-[13px] text-[#6B7280] mt-0.5">
|
||||||
|
مرحبًا بعودتك، <strong className="text-[#111827]">{session.name}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 bg-[#D1FAE5] border border-[#A7F3D0] rounded-full px-3 py-1.5" role="status" aria-label="الحساب نشط">
|
||||||
|
<span aria-hidden="true" className="w-2 h-2 rounded-full bg-[#10B981] motion-safe:animate-pulse" />
|
||||||
|
<span className="text-[11px] font-bold text-[#065F46]">نشط</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Loading */}
|
||||||
|
{loading && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-14 gap-3" role="status">
|
||||||
|
<Spinner />
|
||||||
|
<p className="text-[13px] text-[#6B7280]">جارٍ تحميل الطلبات…</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div className="flex flex-col items-center gap-3 py-10" role="alert">
|
||||||
|
<p className="text-[13px] text-red-500">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && (
|
||||||
|
<>
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-3 mb-6">
|
||||||
|
{[
|
||||||
|
{ label: "إجمالي الطلبات", value: summary.total, color: "text-[#111827]" },
|
||||||
|
{ label: "مُسلَّمة", value: summary.delivered, color: "text-[#34A853]" },
|
||||||
|
{ label: "قيد التنفيذ", value: summary.pending, color: "text-[#1A73E8]" },
|
||||||
|
].map(s => (
|
||||||
|
<div key={s.label} className="bg-white border border-[#E5E7EB] rounded-xl p-3.5">
|
||||||
|
<p className="text-[10px] font-bold text-[#9CA3AF] mb-1">{s.label}</p>
|
||||||
|
<p className={`text-[26px] font-bold ${s.color}`}>{s.value}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Active order tracker */}
|
||||||
|
{activeOrder ? (
|
||||||
|
<div className="bg-gradient-to-bl from-[#0D47A1] to-[#1A73E8] rounded-xl p-4 mb-6">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-bold text-white/60 tracking-wide">الطلب الحالي</p>
|
||||||
|
<p className="text-[14px] font-bold text-white font-mono">{activeOrder.id}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`text-[11px] font-bold border px-2.5 py-1 rounded-full ${
|
||||||
|
activeOrder.status === "CREATED"
|
||||||
|
? "bg-white/15 border-white/30 text-white"
|
||||||
|
: "bg-[#D1FAE5] border-[#A7F3D0] text-[#065F46]"
|
||||||
|
}`}>
|
||||||
|
{statusLabel(activeOrder.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<OrderTracker status={activeOrder.status} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="border border-dashed border-[#E5E7EB] rounded-xl p-5 mb-6 text-center">
|
||||||
|
<p className="text-[13px] text-[#9CA3AF]">لا يوجد طلب نشط حاليًا.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Past orders table */}
|
||||||
|
{pastOrders.length > 0 && (
|
||||||
|
<div className="bg-white border border-[#E5E7EB] rounded-xl overflow-hidden">
|
||||||
|
<div className="px-4 py-3 border-b border-[#E5E7EB] flex items-center justify-between">
|
||||||
|
<p className="text-[13px] font-bold text-[#111827]">سجل الطلبات</p>
|
||||||
|
<span className="text-[11px] text-[#9CA3AF] font-medium">{pastOrders.length} طلبات</span>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full" dir="rtl">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-[#F9FAFB]">
|
||||||
|
{["رقم الطلب", "التاريخ", "الوصف", "الحالة", "المبلغ"].map(h => (
|
||||||
|
<th key={h} scope="col" className="px-4 py-2.5 text-right text-[10px] font-bold text-[#9CA3AF] uppercase tracking-wide">
|
||||||
|
{h}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{pastOrders.map((order, i) => (
|
||||||
|
<tr key={order.id} className={`border-t border-[#F3F4F6] hover:bg-[#FAFAFA] transition-colors ${i % 2 === 0 ? "" : "bg-[#FAFAFA]/30"}`}>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className="font-mono text-[12px] font-semibold text-[#1A73E8]">{order.id}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-[12px] text-[#6B7280] whitespace-nowrap">{fmtDate(order.createdAt)}</td>
|
||||||
|
<td className="px-4 py-3 text-[12px] text-[#374151]">{order.description}</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className={`inline-flex items-center gap-1.5 text-[11px] font-bold border px-2.5 py-0.5 rounded-full whitespace-nowrap ${statusColor(order.status)}`}>
|
||||||
|
<span aria-hidden="true" className="w-1.5 h-1.5 rounded-full bg-current" />
|
||||||
|
{statusLabel(order.status)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-[12px] font-semibold text-[#111827] font-mono whitespace-nowrap">
|
||||||
|
{fmtAmount(order.totalAmount)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { clearSession(); window.location.reload(); }}
|
||||||
|
className="mt-4 text-[11px] text-[#9CA3AF] hover:text-red-400 underline transition-colors"
|
||||||
|
>
|
||||||
|
مسح الجلسة وإعادة البدء
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardStep;
|
||||||
99
src/Components/Trip/Tripdeletemodal.tsx
Normal file
99
src/Components/Trip/Tripdeletemodal.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="alertdialog" aria-modal="true" aria-labelledby="trip-del-title"
|
||||||
|
onClick={e => { 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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => 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 */}
|
||||||
|
<div style={{
|
||||||
|
width: 52, height: 52, margin: "0 auto", borderRadius: "50%",
|
||||||
|
background: "#FEF2F2", border: "1px solid #FECACA",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
}}>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#DC2626" strokeWidth="2">
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" />
|
||||||
|
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 id="trip-del-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
|
||||||
|
حذف الرحلة
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginTop: 8, fontSize: 13, color: "var(--color-text-muted)", lineHeight: 1.6 }}>
|
||||||
|
هل أنت متأكد من حذف رحلة{" "}
|
||||||
|
<strong style={{ color: "var(--color-text-primary)" }}>{trip.title}</strong>
|
||||||
|
{" "}(
|
||||||
|
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12 }}>
|
||||||
|
{trip.tripNumber}
|
||||||
|
</span>
|
||||||
|
)؟ لا يمكن التراجع عن هذا الإجراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button type="button" onClick={onCancel} disabled={deleting}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: 40, borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-border)", background: "var(--color-surface)",
|
||||||
|
fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)",
|
||||||
|
cursor: deleting ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)",
|
||||||
|
}}>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||||
|
style={{
|
||||||
|
flex: 1, height: 40, borderRadius: "var(--radius-md)",
|
||||||
|
border: "none", background: "#DC2626",
|
||||||
|
fontSize: 13, fontWeight: 700, color: "#FFF",
|
||||||
|
cursor: deleting ? "not-allowed" : "pointer",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center", gap: 8,
|
||||||
|
fontFamily: "var(--font-sans)", opacity: deleting ? 0.7 : 1,
|
||||||
|
}}>
|
||||||
|
{deleting && <Spinner size="sm" className="text-white" />}
|
||||||
|
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
167
src/Components/Trip/archive/ArchivedTripList.tsx
Normal file
167
src/Components/Trip/archive/ArchivedTripList.tsx
Normal file
@@ -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 (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11px] font-bold"
|
||||||
|
style={{ color: s.color, background: s.bg, border: `1px solid ${s.border}` }}
|
||||||
|
>
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full" style={{ background: s.dot }} />
|
||||||
|
{s.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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
|
||||||
|
* <ArchivedTripList onView={(trip) => setViewTripId(trip.id)} />
|
||||||
|
*/
|
||||||
|
export function ArchivedTripList({ onView }: ArchivedTripListProps) {
|
||||||
|
const {
|
||||||
|
trips, loading, total, pages, page, search, error,
|
||||||
|
setPage, handleSearch, clearError,
|
||||||
|
} = useArchivedTrips();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir="rtl" className="flex flex-col gap-4">
|
||||||
|
{/* ── Header: count + search ── */}
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<h2 className="text-base font-bold text-[var(--color-text-primary)]">
|
||||||
|
الرحلات المؤرشفة
|
||||||
|
<span className="mr-2 text-[13px] font-medium text-[var(--color-text-muted)]">
|
||||||
|
({total})
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="relative w-full sm:w-72">
|
||||||
|
<svg
|
||||||
|
className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--color-text-hint)]"
|
||||||
|
fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="بحث برقم الرحلة أو العنوان..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── API error feedback ── */}
|
||||||
|
{error && <Alert type="error" message={error} onClose={clearError} />}
|
||||||
|
|
||||||
|
{/* ── Card ── */}
|
||||||
|
<div className="overflow-hidden rounded-[var(--radius-xl)] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[var(--shadow-card)]">
|
||||||
|
{/* column headers */}
|
||||||
|
<div className="grid grid-cols-[2fr_1.5fr_1fr_1fr_1fr] gap-2 border-b border-[var(--color-border)] bg-[var(--color-surface-muted)] px-6 py-3 text-[11px] font-bold uppercase tracking-[0.2em] text-[var(--color-text-muted)]">
|
||||||
|
<span>الرحلة</span>
|
||||||
|
<span>الحالة</span>
|
||||||
|
<span className="text-center">البدء</span>
|
||||||
|
<span className="text-center">الانتهاء</span>
|
||||||
|
<span className="text-center">النقد المحصّل</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Loading state ── */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center gap-3 py-16 text-[var(--color-text-muted)]">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
<span className="text-[13px]">جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
) : trips.length === 0 ? (
|
||||||
|
/* ── Empty state ── */
|
||||||
|
<EmptyState
|
||||||
|
icon="🗄️"
|
||||||
|
title="لا توجد رحلات مؤرشفة"
|
||||||
|
description={search ? `لا توجد نتائج لـ "${search}"` : "لم يتم أرشفة أي رحلات بعد."}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
/* ── Data rows ── */
|
||||||
|
<ul className="m-0 list-none p-0">
|
||||||
|
{trips.map((trip, i) => (
|
||||||
|
<li
|
||||||
|
key={trip.id}
|
||||||
|
onClick={() => 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" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="m-0 truncate font-semibold text-[var(--color-text-primary)]">{trip.title}</p>
|
||||||
|
<p className="mt-0.5 font-mono text-[11px] text-[var(--color-brand-600)]">{trip.tripNumber}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TripStatusBadge status={trip.status} />
|
||||||
|
</div>
|
||||||
|
<span className="text-center text-[12px] text-[var(--color-text-muted)]">
|
||||||
|
{fmtDate(trip.startTime)}
|
||||||
|
</span>
|
||||||
|
<span className="text-center text-[12px] text-[var(--color-text-muted)]">
|
||||||
|
{fmtDate(trip.endTime)}
|
||||||
|
</span>
|
||||||
|
<span className="text-center font-mono text-[12px] font-semibold text-[var(--color-text-primary)]">
|
||||||
|
{trip.totalCashCollected != null
|
||||||
|
? `${Number(trip.totalCashCollected).toLocaleString("ar-SA")} ر.س`
|
||||||
|
: "—"}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Pagination ── */}
|
||||||
|
{pages > 1 && (
|
||||||
|
<div className="flex items-center justify-between border-t border-[var(--color-border)] px-6 py-3.5">
|
||||||
|
<span className="text-[12px] text-[var(--color-text-muted)]">
|
||||||
|
صفحة <strong className="text-[var(--color-text-primary)]">{page}</strong> من{" "}
|
||||||
|
<strong className="text-[var(--color-text-primary)]">{pages}</strong>
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={page === 1}
|
||||||
|
onClick={() => setPage(Math.max(1, page - 1))}
|
||||||
|
className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3.5 py-1.5 text-[12px] text-[var(--color-text-secondary)] disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
>
|
||||||
|
السابق
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={page === pages}
|
||||||
|
onClick={() => setPage(Math.min(pages, page + 1))}
|
||||||
|
className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3.5 py-1.5 text-[12px] text-[var(--color-text-secondary)] disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
>
|
||||||
|
التالي
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/Components/User/DeleteConfirmModal.tsx
Normal file
58
src/Components/User/DeleteConfirmModal.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="alertdialog" aria-modal="true" aria-labelledby="del-title"
|
||||||
|
onClick={e => { 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" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => 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 */}
|
||||||
|
<div style={{ width: 52, height: 52, margin: "0 auto", borderRadius: "50%", background: "#FEF2F2", border: "1px solid #FECACA", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#DC2626" strokeWidth="2">
|
||||||
|
<polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" /><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 id="del-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>حذف المستخدم</h2>
|
||||||
|
<p style={{ marginTop: 8, fontSize: 13, color: "var(--color-text-muted)", lineHeight: 1.6 }}>
|
||||||
|
هل أنت متأكد من حذف <strong style={{ color: "var(--color-text-primary)" }}>{user.name}</strong>؟ لا يمكن التراجع عن هذا الإجراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button type="button" onClick={onCancel} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)", cursor: deleting ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)" }}>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "none", background: "#DC2626", fontSize: 13, fontWeight: 700, color: "#FFF", cursor: deleting ? "not-allowed" : "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 8, fontFamily: "var(--font-sans)", opacity: deleting ? 0.7 : 1 }}>
|
||||||
|
{deleting && <Spinner size="sm" className="text-white" />}
|
||||||
|
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
src/Components/User/Toast.tsx
Normal file
56
src/Components/User/Toast.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="status" aria-live="polite" aria-atomic="true"
|
||||||
|
style={{
|
||||||
|
position: "fixed", bottom: 24, left: "50%",
|
||||||
|
transform: `translateX(-50%) translateY(${notification ? "0" : "16px"})`,
|
||||||
|
zIndex: 9999,
|
||||||
|
transition: "transform 250ms ease, opacity 250ms ease",
|
||||||
|
opacity: notification ? 1 : 0,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
display: "flex", alignItems: "center", gap: 10,
|
||||||
|
padding: "0.75rem 1.25rem",
|
||||||
|
borderRadius: "var(--radius-full)",
|
||||||
|
background: isSuccess ? "#065F46" : "#7F1D1D",
|
||||||
|
color: "#FFFFFF",
|
||||||
|
fontSize: 13, fontWeight: 600,
|
||||||
|
boxShadow: "0 8px 32px rgba(0,0,0,0.25)",
|
||||||
|
maxWidth: "90vw", whiteSpace: "nowrap",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
}}>
|
||||||
|
{/* icon */}
|
||||||
|
<span style={{ fontSize: 16 }}>{isSuccess ? "✓" : "⚠"}</span>
|
||||||
|
<span>{notification?.message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
src/Components/car/CarDeleteModal.tsx
Normal file
86
src/Components/car/CarDeleteModal.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="alertdialog" aria-modal="true" aria-labelledby="car-del-title"
|
||||||
|
onClick={e => { 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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => 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 */}
|
||||||
|
<div style={{ width: 52, height: 52, margin: "0 auto", borderRadius: "50%", background: "#FEF2F2", border: "1px solid #FECACA", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#DC2626" strokeWidth="2">
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" />
|
||||||
|
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 id="car-del-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
|
||||||
|
حذف المركبة
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginTop: 8, fontSize: 13, color: "var(--color-text-muted)", lineHeight: 1.6 }}>
|
||||||
|
هل أنت متأكد من حذف{" "}
|
||||||
|
<strong style={{ color: "var(--color-text-primary)" }}>
|
||||||
|
{car.manufacturer} {car.model}
|
||||||
|
</strong>
|
||||||
|
{" "}(
|
||||||
|
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12 }}>
|
||||||
|
{car.plateLetters} {car.plateNumber}
|
||||||
|
</span>
|
||||||
|
)؟ لا يمكن التراجع عن هذا الإجراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button type="button" onClick={onCancel} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)", cursor: deleting ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)" }}>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "none", background: "#DC2626", fontSize: 13, fontWeight: 700, color: "#FFF", cursor: deleting ? "not-allowed" : "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 8, fontFamily: "var(--font-sans)", opacity: deleting ? 0.7 : 1 }}>
|
||||||
|
{deleting && <Spinner size="sm" className="text-white" />}
|
||||||
|
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
src/Components/role/DeleteRoleModal.tsx
Normal file
74
src/Components/role/DeleteRoleModal.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
role="alertdialog" aria-modal="true" aria-labelledby="del-role-title"
|
||||||
|
onClick={e => { 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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div onClick={e => 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 */}
|
||||||
|
<div style={{ width: 52, height: 52, margin: "0 auto", borderRadius: "50%", background: "#FEF2F2", border: "1px solid #FECACA", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#DC2626" strokeWidth="2">
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
||||||
|
<path d="M10 11v6M14 11v6" />
|
||||||
|
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 id="del-role-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
|
||||||
|
حذف الدور
|
||||||
|
</h2>
|
||||||
|
<p style={{ marginTop: 8, fontSize: 13, color: "var(--color-text-muted)", lineHeight: 1.6 }}>
|
||||||
|
هل أنت متأكد من حذف دور <strong style={{ color: "var(--color-text-primary)" }}>{role.name}</strong>؟
|
||||||
|
لا يمكن التراجع عن هذا الإجراء.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
<button type="button" onClick={onCancel} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)", cursor: deleting ? "not-allowed" : "pointer", fontFamily: "var(--font-sans)" }}>
|
||||||
|
إلغاء
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onConfirm} disabled={deleting}
|
||||||
|
style={{ flex: 1, height: 40, borderRadius: "var(--radius-md)", border: "none", background: "#DC2626", fontSize: 13, fontWeight: 700, color: "#FFF", cursor: deleting ? "not-allowed" : "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 8, fontFamily: "var(--font-sans)", opacity: deleting ? 0.7 : 1 }}>
|
||||||
|
{deleting && <Spinner size="sm" className="text-white" />}
|
||||||
|
{deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/hooks/useClientOrders.ts
Normal file
45
src/hooks/useClientOrders.ts
Normal file
@@ -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<State>({
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user