finsh user,car,driver pages also add validator layer to app build trip page

This commit is contained in:
m7amedez5511
2026-06-18 16:51:31 +03:00
parent a23d21f222
commit c1b77f89cd
45 changed files with 4128 additions and 1690 deletions

View File

@@ -1,95 +0,0 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { loadSession, type ClientSession } from "@/lib/session";
import RegisterStep from "../..//Components/Client/client";
import DashboardStep from "../../Components/Order/order";
import AddressStep from "../../Components/Client_Adress/clientAdress";
import Logo from "../../utils/logo";
import { Spinner } from "../../Components/UI";
type Step = "bootstrap" | "register" | "address" | "dashboard";
function Navbar({ session }: { session: ClientSession | null }) {
return (
<nav
aria-label="Client portal navigation"
className="bg-white border-b border-[#E5E7EB] flex items-center justify-between px-5 h-14 sticky top-0 z-30"
>
<div className="flex items-center gap-2">
{session?.name && (
<span className="text-[12px] text-[#6B7280] hidden sm:block">{session.name}</span>
)}
<div aria-hidden="true" className="w-8 h-8 rounded-full bg-[#EBF3FF] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1A73E8" strokeWidth="2">
<circle cx="12" cy="8" r="4"/>
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
</svg>
</div>
</div>
<Logo />
</nav>
);
}
export default function ClientOrderPage() {
const [step, setStep] = useState<Step>("bootstrap");
const [session, setSession] = useState<ClientSession | null>(null);
useEffect(() => {
const saved = loadSession();
if (saved?.id && saved.name) {
setSession(saved);
setStep("dashboard");
} else {
setStep("register");
}
}, []);
const handleRegistered = useCallback((s: ClientSession) => {
setSession(s);
setStep("address");
}, []);
const handleAddressComplete = useCallback(() => {
setStep("dashboard");
}, []);
return (
<div
className="min-h-screen bg-[#ECEEF2]"
dir="rtl"
lang="ar"
style={{ fontFamily: "var(--font-sans)" }}
>
<Navbar session={session} />
<div className="max-w-lg mx-auto px-4 py-8">
<div className="bg-white border border-[#E5E7EB] rounded-2xl p-6 shadow-sm">
{step === "bootstrap" && (
<div className="flex flex-col items-center justify-center py-10 gap-3" role="status">
<Spinner size="lg" />
<p className="text-[13px] text-[#6B7280]">جارٍ تحميل بيانات الجلسة</p>
</div>
)}
{step === "register" && (
<RegisterStep onSuccess={handleRegistered} />
)}
{step === "address" && session && (
<AddressStep session={session} onSuccess={handleAddressComplete} />
)}
{step === "dashboard" && session && (
<DashboardStep session={session} />
)}
</div>
<p className="text-center text-[11px] text-[#9CA3AF] mt-4">
LogiFlow · آمن ومشفر · المملكة العربية السعودية 🇸🇦
</p>
</div>
</div>
);
}

View File

@@ -34,9 +34,7 @@ export default function Navbar() {
</div>
<div className="mr-auto flex items-center gap-2">
<Link href="/NewOrder" className="flex h-[34px] items-center rounded-lg border border-slate-200 bg-white px-3 text-[13px] font-semibold text-slate-700 shadow-sm transition hover:bg-slate-50">
إنشاء طلب جديد
</Link>
<Link href="/login" className="flex h-[34px] items-center rounded-lg bg-blue-600 px-3 text-[13px] font-bold text-white shadow-sm transition hover:bg-blue-700">
تسجيل الدخول
</Link>

View File

@@ -10,25 +10,26 @@ const navSections = [
label: "الرئيسية",
items: [
{ href: "/dashboard", label: "Dashboard", icon: "ti-layout-dashboard", permission: "read-dashboard" },
{ href: "/users", label: "Users", icon: "ti-users", permission: "read-user" },
{ href: "/dashboard/users", label: "Users", icon: "ti-users", permission: "read-user" },
],
},
{
label: "الأسطول",
items: [
{ href: "/cars", label: "Cars", icon: "ti-car", permission: "read-car" },
{ href: "/drivers", label: "Drivers", icon: "ti-steering-wheel",permission: "read-driver" },
{ href: "/trips", label: "Trips", icon: "ti-route", permission: "read-trip" },
{ href: "/dashboard/cars", label: "Cars", icon: "ti-car", permission: "read-car" },
{ href: "/dashboard/drivers", label: "Drivers", icon: "ti-steering-wheel",permission: "read-driver" },
{ href: "/dashboard/trips", label: "Trips", icon: "ti-route", permission: "read-trip" },
],
},
{
label: "العمليات",
items: [
{ href: "/orders", label: "Orders", icon: "ti-package", permission: "read-order" },
{ href: "/clients", label: "Clients", icon: "ti-users-group", permission: "read-client" },
{ href: "/branches", label: "Branches", icon: "ti-building", permission: "read-branch" },
{ href: "/roles", label: "Roles", icon: "ti-shield", permission: "read-role" },
{ href: "/audit", label: "Audit", icon: "ti-clipboard-list", permission: "read-audit" },
{ href: "/dashboard/orders", label: "Orders", icon: "ti-package", permission: "read-order" },
{ href: "/dashboard/clients", label: "Clients", icon: "ti-users-group", permission: "read-client" },
{ href: "/dashboard/branches", label: "Branches", icon: "ti-building", permission: "read-branch" },
{ href: "/dashboard/roles", label: "Roles", icon: "ti-shield", permission: "read-role" },
{ href: "/dashboard/role_permission", label: "Roles & Permission", icon: "ti-shield", permission: "manage-role-permissions" },
{ href: "/dashboard/audit", label: "Audit", icon: "ti-clipboard-list", permission: "read-audit" },
],
},
];

View File

@@ -1,68 +1,25 @@
"use client";
// app/(dashboard)/cars/page.tsx
import { useCallback, useEffect, useState } from "react";
import { getStoredToken } from "../../lib/auth";
import { carService } from "../../services/car.service";
import { get } from "../../services/api";
import { Spinner, Alert } from "../../Components/UI";
import { CarFormModal } from "../../Components/car/CarFormModal";
import { CarDetailPanel } from "../../Components/car/CarDetailPanel";
import { CarDeleteModal } from "../../Components/car/CarDeleteModal";
import type { Car, CreateCarPayload, UpdateCarPayload } from "../../types/car";
import { useCallback, useState } from "react";
import { Spinner, Alert } from "@/Components/UI";
import { CarFormModal } from "@/Components/car/CarFormModal";
import { CarDetailPanel } from "@/Components/car/CarDetailPanel";
import { CarDeleteModal } from "@/Components/car/CarDeleteModal";
import { useCars, useCarMutations, useToast } from "@/hooks/useCars";
import { fmtDateShort, isExpiringSoon, STATUS_MAP, INS_MAP } from "@/types/car";
import type { Car, CreateCarPayload, ToastMsg, UpdateCarPayload } from "@/types/car";
// ── API response shape ────────────────────────────────────────────────────────
interface ApiResponse {
data: {
data: Car[];
pagination: { total: number; page: number; pages: number };
meta?: { total: number; pages: number };
};
}
// ── Status badge config ───────────────────────────────────────────────────────
const STATUS_MAP: Record<
Car["currentStatus"],
{ label: string; color: string; bg: string; border: string; dot: string }
> = {
Active: { label: "نشط", color: "#166534", bg: "#DCFCE7", border: "#BBF7D0", dot: "#16A34A" },
InMaintenance: { label: "صيانة", color: "#854D0E", bg: "#FFFBEB", border: "#FDE68A", dot: "#D97706" },
InTrip: { label: "في رحلة", color: "#1E40AF", bg: "#EFF6FF", border: "#BFDBFE", dot: "#3B82F6" },
Inactive: { label: "غير نشط", color: "#64748B", bg: "#F1F5F9", border: "#E2E8F0", dot: "#94A3B8" },
};
const INS_MAP: Record<string, { label: string; color: string }> = {
Valid: { label: "سارٍ", color: "#16A34A" },
Expired: { label: "منتهي", color: "#DC2626" },
NotInsured: { label: "غير مؤمَّن", color: "#D97706" },
};
function fmtDate(iso?: string) {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" });
}
function isExpiringSoon(iso?: string) {
if (!iso) return false;
return (new Date(iso).getTime() - Date.now()) / 86_400_000 <= 90;
}
// ── Toast (inline — mirrors Components/User/Toast pattern) ───────────────────
interface ToastMsg { type: "success" | "error"; message: string }
// ── Toast ─────────────────────────────────────────────────────────────────────
function CarToast({ notification }: { notification: ToastMsg | null }) {
const [vis, setVis] = useState(false);
useEffect(() => {
if (notification) setVis(true);
else { const t = setTimeout(() => setVis(false), 300); return () => clearTimeout(t); }
}, [notification]);
if (!vis && !notification) return null;
const ok = notification?.type === "success";
if (!notification) return null;
const ok = notification.type === "success";
return (
<div role="status" aria-live="polite" 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",
transform: "translateX(-50%)",
zIndex: 9999, pointerEvents: "none",
}}>
<div style={{
display: "flex", alignItems: "center", gap: 10,
@@ -75,17 +32,18 @@ function CarToast({ notification }: { notification: ToastMsg | null }) {
fontFamily: "var(--font-sans)",
}}>
<span style={{ fontSize: 16 }}>{ok ? "✓" : "⚠"}</span>
<span>{notification?.message}</span>
<span>{notification.message}</span>
</div>
</div>
);
}
// ── Vehicle Card component ────────────────────────────────────────────────────
// ── CarCard ───────────────────────────────────────────────────────────────────
function CarCard({ car, onClick }: { car: Car; onClick: () => void }) {
const status = STATUS_MAP[car.currentStatus];
const ins = car.insuranceStatus ? INS_MAP[car.insuranceStatus] : null;
const regWarn = isExpiringSoon(car.registrationExpiryDate as string | undefined);
const regWarn = isExpiringSoon(car.registrationExpiryDate);
return (
<article
@@ -145,7 +103,7 @@ function CarCard({ car, onClick }: { car: Car; onClick: () => void }) {
</span>
</div>
{/* Plate number — prominent */}
{/* Plate number */}
<div style={{
marginTop: "0.875rem",
background: "var(--color-surface-muted)",
@@ -166,34 +124,24 @@ function CarCard({ car, onClick }: { car: Car; onClick: () => void }) {
{/* Key attributes grid */}
<div style={{ marginTop: "0.875rem", display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.5rem" }}>
{/* Branch */}
<div style={{ fontSize: 11 }}>
<span style={{ color: "var(--color-text-muted)", fontWeight: 600, display: "block" }}>الفرع</span>
<span style={{ color: "var(--color-text-primary)", marginTop: 2, display: "block" }}>
{car.branch?.name ?? "—"}
</span>
</div>
{/* Insurance */}
<div style={{ fontSize: 11 }}>
<span style={{ color: "var(--color-text-muted)", fontWeight: 600, display: "block" }}>التأمين</span>
<span style={{
color: ins?.color ?? "var(--color-text-muted)",
marginTop: 2, display: "block", fontWeight: 600,
}}>
<span style={{ color: ins?.color ?? "var(--color-text-muted)", marginTop: 2, display: "block", fontWeight: 600 }}>
{ins?.label ?? "—"}
</span>
</div>
{/* Registration expiry */}
<div style={{ fontSize: 11 }}>
<span style={{ color: "var(--color-text-muted)", fontWeight: 600, display: "block" }}>انتهاء الاستمارة</span>
<span style={{
color: regWarn ? "#D97706" : "var(--color-text-secondary)",
marginTop: 2, display: "block", fontWeight: regWarn ? 600 : 400,
}}>
{regWarn && "⚠ "}{fmtDate(car.registrationExpiryDate as string | undefined)}
<span style={{ color: regWarn ? "#D97706" : "var(--color-text-secondary)", marginTop: 2, display: "block", fontWeight: regWarn ? 600 : 400 }}>
{regWarn && "⚠ "}{fmtDateShort(car.registrationExpiryDate)}
</span>
</div>
{/* Capacity */}
<div style={{ fontSize: 11 }}>
<span style={{ color: "var(--color-text-muted)", fontWeight: 600, display: "block" }}>الطاقة</span>
<span style={{ color: "var(--color-text-secondary)", marginTop: 2, display: "block" }}>
@@ -203,11 +151,7 @@ function CarCard({ car, onClick }: { car: Car; onClick: () => void }) {
</div>
{/* Footer CTA hint */}
<p style={{
marginTop: "0.875rem",
fontSize: 11, color: "var(--color-brand-600)",
fontWeight: 600, textAlign: "left",
}}>
<p style={{ marginTop: "0.875rem", fontSize: 11, color: "var(--color-brand-600)", fontWeight: 600, textAlign: "left" }}>
اضغط لعرض التفاصيل
</p>
</div>
@@ -216,96 +160,43 @@ function CarCard({ car, onClick }: { car: Car; onClick: () => void }) {
}
// ── Page ──────────────────────────────────────────────────────────────────────
export default function CarsPage() {
// List state
const [cars, setCars] = useState<Car[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [pages, setPages] = useState(1);
// Modal state
const [detailId, setDetailId] = useState<string | null>(null); // CarDetailPanel
const [formTarget, setFormTarget] = useState<Car | null | false>(false); // false=closed
const [detailId, setDetailId] = useState<string | null>(null);
const [formTarget, setFormTarget] = useState<Car | null | false>(false); // false = closed
const [deleteTarget, setDeleteTarget] = useState<Car | null>(null);
const [deleting, setDeleting] = useState(false);
// Toast
const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null);
const notify = useCallback((t: typeof toast) => {
setToast(t);
setTimeout(() => setToast(null), 3500);
}, []);
const { toast, notify } = useToast();
// ── Fetch list ─────────────────────────────────────────────────────────────
const loadCars = useCallback(() => {
const token = getStoredToken();
setLoading(true);
setError(null);
const query = `?page=${page}&limit=12${search ? `&search=${encodeURIComponent(search)}` : ""}`;
get<ApiResponse>(`cars${query}`, token)
.then((res) => {
const payload = (res as unknown as { data: ApiResponse["data"] }).data ?? res;
setCars((payload as ApiResponse["data"]).data ?? []);
setTotal((payload as ApiResponse["data"]).meta?.total ?? 0);
setPages((payload as ApiResponse["data"]).meta?.pages ?? 1);
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [page, search]);
const { cars, loading, error, total, pages, loadCars, removeCar, setError } =
useCars(page, search);
useEffect(() => { loadCars(); }, [loadCars]);
// Need a stable ref to the current edit target for the mutation hook
const getEditTarget = useCallback(() =>
formTarget instanceof Object && formTarget !== null ? formTarget as Car : null,
[formTarget]);
// ── Create / Update ────────────────────────────────────────────────────────
const handleFormSubmit = async (
payload: CreateCarPayload | UpdateCarPayload,
isNew: boolean,
): Promise<boolean> => {
const token = getStoredToken();
try {
if (isNew) {
await carService.create(payload as CreateCarPayload, token);
notify({ type: "success", message: "تم إضافة المركبة بنجاح." });
} else {
await carService.update((formTarget as Car).id, payload as UpdateCarPayload, token);
notify({ type: "success", message: "تم تحديث بيانات المركبة." });
}
loadCars();
return true;
} catch (err: unknown) {
notify({ type: "error", message: err instanceof Error ? err.message : "فشلت العملية." });
return false;
}
};
const { deleting, handleFormSubmit, handleDeleteConfirm } = useCarMutations({
onSuccess: (msg) => { notify({ type: "success", message: msg }); loadCars(); },
onError: (msg) => notify({ type: "error", message: msg }),
onDeleted: (id) => { removeCar(id); setDeleteTarget(null); },
getEditTarget,
});
// ── Delete ─────────────────────────────────────────────────────────────────
const handleDeleteConfirm = async () => {
const handleDelete = async () => {
if (!deleteTarget) return;
setDeleting(true);
const token = getStoredToken();
try {
await carService.delete(deleteTarget.id, token);
// Optimistic removal — no page refresh needed
setCars(prev => prev.filter(c => c.id !== deleteTarget.id));
setTotal(prev => Math.max(0, prev - 1));
setDeleteTarget(null);
notify({ type: "success", message: `تم حذف ${deleteTarget.manufacturer} ${deleteTarget.model} بنجاح.` });
} catch (err: unknown) {
notify({ type: "error", message: err instanceof Error ? err.message : "فشل الحذف." });
} finally {
setDeleting(false);
}
await handleDeleteConfirm(deleteTarget);
};
// ── Render ─────────────────────────────────────────────────────────────────
// ── Render ─────────────────────────────────────────────────────────────────
return (
<>
{/* Toast notification */}
<CarToast notification={toast} />
{/* Detail panel (slide-in) */}
{detailId && (
<CarDetailPanel
carId={detailId}
@@ -315,23 +206,23 @@ export default function CarsPage() {
/>
)}
{/* Create / Edit modal */}
{formTarget !== false && (
<CarFormModal
editCar={formTarget}
branches={[]} // loaded inside modal via its own effect in production
branches={[]}
onClose={() => setFormTarget(false)}
onSubmit={handleFormSubmit}
onSubmit={(payload: CreateCarPayload | UpdateCarPayload, isNew: boolean) =>
handleFormSubmit(payload, isNew).then((ok) => { if (ok) setFormTarget(false); return ok; })
}
/>
)}
{/* Delete confirmation */}
{deleteTarget && (
<CarDeleteModal
car={deleteTarget}
deleting={deleting}
onCancel={() => setDeleteTarget(null)}
onConfirm={handleDeleteConfirm}
onConfirm={handleDelete}
/>
)}
@@ -418,7 +309,6 @@ export default function CarsPage() {
<span style={{ fontSize: 14 }}>جارٍ تحميل المركبات</span>
</div>
) : cars.length === 0 ? (
/* Empty state */
<div style={{
borderRadius: "var(--radius-xl)",
border: "2px dashed var(--color-border)",
@@ -431,7 +321,7 @@ export default function CarsPage() {
{search ? `لا توجد مركبات تطابق "${search}"` : "لا توجد مركبات بعد"}
</p>
<p style={{ fontSize: 13, color: "var(--color-text-muted)", marginTop: 6 }}>
اضغط على "إضافة مركبة" لإضافة أول مركبة في الأسطول.
اضغط على &quot;إضافة مركبة&quot; لإضافة أول مركبة في الأسطول.
</p>
{!search && (
<button

View File

@@ -2,16 +2,16 @@
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Alert } from "../../../../Components/UI";
import { Toast } from "../../../../Components/Client/Toast";
import { useClientAddresses } from "../../../../hooks/useClientAddresses";
import { clientService } from "../../../../services/client.service";
import { getStoredToken } from "../../../../lib/auth";
import { Alert } from "@/Components/UI";
import { Toast } from "@/Components/Client/Toast";
import { useClientAddresses } from "@/hooks/useClientAddresses";
import { clientService } from "@/services/client.service";
import { getStoredToken } from "@/lib/auth";
import type {
Client,
ClientAddress,
ClientAddressFormData,
} from "../../../../types/client";
} from "@/types/client";
import { AddressFormModal } from "@/Components/Client/Addressformmodal";
import { DeleteConfirmModal } from "@/Components/Client/Deleteconfirmmodal";

View File

@@ -2,10 +2,10 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Alert } from "../../Components/UI";
import { Toast } from "../../Components/Client/Toast";
import { useClients } from "../../hooks/useClients";
import type { Client, ClientFormData } from "../../types/client";
import { Alert } from "@/Components/UI";
import { Toast } from "@/Components/Client/Toast";
import { useClients } from "@/hooks/useClients";
import type { Client, ClientFormData } from "@/types/client";
import { ClientFormModal } from "@/Components/Client/Clientformmodal";
import { ClientTable } from "@/Components/Client/Clienttable";
import { DeleteConfirmModal } from "@/Components/Client/Deleteconfirmmodal";
@@ -48,7 +48,7 @@ export default function ClientsPage() {
// ── Navigate to address management ──────────────────────────────────────
const handleManageAddresses = (client: Client) => {
router.push(`/clients/${client.id}/addresses`);
router.push(`/dashboard/clients/${client.id}/addresses`);
};
// ── Render ───────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,624 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Spinner } from "@/Components/UI";
import { DriverFormModal } from "@/Components/Driver/DriverFormModal";
import { DriverDeleteModal } from "@/Components/Driver/DriverDeleteModal";
import { DriverReportPanel } from "@/Components/Driver_Report/driverReport";
import { driverService } from "@/services";
import { getStoredToken } from "@/lib/auth";
import type {
Driver,
CreateDriverPayload,
UpdateDriverPayload,
} from "@/types/driver";
import {
DRIVER_STATUS_MAP,
DRIVER_CARD_TYPE_MAP,
NATIONAL_ID_TYPE_MAP,
} from "@/types/driver";
// ── Helpers ───────────────────────────────────────────────────────────────────
function fmtDate(iso?: string | null): string {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("ar-SA", {
year: "numeric",
month: "long",
day: "numeric",
});
}
function isExpiringSoon(iso?: string | null): boolean {
if (!iso) return false;
return (new Date(iso).getTime() - Date.now()) <= 90 * 86_400_000;
}
// ── Sub-components ────────────────────────────────────────────────────────────
function SectionCard({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div
style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
overflow: "hidden",
boxShadow: "var(--shadow-card)",
}}
>
<div
style={{
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
}}
>
<p
style={{
fontSize: 11,
letterSpacing: "0.25em",
textTransform: "uppercase",
color: "var(--color-text-hint)",
fontWeight: 700,
margin: 0,
}}
>
{title}
</p>
</div>
<div style={{ padding: "1rem 1.5rem" }}>{children}</div>
</div>
);
}
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 PhotoCard({ url, label }: { url?: string | null; label: string }) {
const [imgError, setImgError] = useState(false);
return (
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<span style={{ fontSize: 12, fontWeight: 600, color: "var(--color-text-muted)" }}>
{label}
</span>
<div
style={{
width: "100%",
aspectRatio: "4/3",
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{url && !imgError ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={url}
alt={label}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
onError={() => setImgError(true)}
/>
) : (
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="var(--color-text-hint)"
strokeWidth="1.5"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
)}
</div>
{url && !imgError && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
style={{ fontSize: 11, color: "#2563EB", textDecoration: "underline", textAlign: "center" }}
>
عرض الصورة
</a>
)}
</div>
);
}
// ── Page ──────────────────────────────────────────────────────────────────────
export default function DriverDetailPage() {
const params = useParams();
const router = useRouter();
const driverId = params?.driverId as string;
const [driver, setDriver] = useState<Driver | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [avatarError, setAvatarError] = useState(false);
// ── Modal state ───────────────────────────────────────────────────────────
const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
// ── Load driver ───────────────────────────────────────────────────────────
const loadDriver = useCallback(async () => {
if (!driverId) return;
setLoading(true);
setError(null);
try {
const token = getStoredToken();
const res = await driverService.getById(driverId, token);
setDriver((res as unknown as { data: Driver }).data);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "تعذّر تحميل بيانات السائق.");
} finally {
setLoading(false);
}
}, [driverId]);
useEffect(() => {
loadDriver();
}, [loadDriver]);
// ── Edit submit ───────────────────────────────────────────────────────────
const handleEditSubmit = useCallback(
async (
payload: CreateDriverPayload | UpdateDriverPayload,
_isNew: boolean,
): Promise<boolean> => {
if (!driver) return false;
try {
const token = getStoredToken();
await driverService.update(driver.id, payload as UpdateDriverPayload, token);
await loadDriver();
return true;
} catch {
return false;
}
},
[driver, loadDriver],
);
// ── Delete confirm ────────────────────────────────────────────────────────
const handleConfirmDelete = useCallback(async () => {
if (!driver) return;
setDeleting(true);
try {
const token = getStoredToken();
await driverService.delete(driver.id, token);
router.push("/dashboard/drivers");
} catch {
setDeleting(false);
}
}, [driver, router]);
// ── Status config ─────────────────────────────────────────────────────────
const statusConfig = driver ? DRIVER_STATUS_MAP[driver.status] : null;
// ── Render: Loading ───────────────────────────────────────────────────────
if (loading) {
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 12,
padding: "6rem 0",
color: "var(--color-text-muted)",
}}
>
<Spinner size="sm" className="text-blue-600" />
<span style={{ fontSize: 14 }}>جارٍ التحميل</span>
</div>
);
}
// ── Render: Error ─────────────────────────────────────────────────────────
if (error || !driver) {
return (
<div
style={{
maxWidth: 480,
margin: "4rem auto",
borderRadius: "var(--radius-xl)",
border: "1px solid #FECACA",
background: "#FEF2F2",
padding: "1.5rem",
textAlign: "center",
}}
>
<p style={{ fontSize: 14, color: "#DC2626", fontWeight: 600 }}>
{error ?? "السائق غير موجود"}
</p>
<button
type="button"
onClick={() => router.back()}
style={{
marginTop: "1rem",
height: 38,
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: "pointer",
fontFamily: "var(--font-sans)",
}}
>
رجوع
</button>
</div>
);
}
// ── Render: Driver detail ─────────────────────────────────────────────────
return (
<>
<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.back()}
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
fontSize: 12,
fontWeight: 600,
color: "var(--color-text-muted)",
background: "none",
border: "none",
cursor: "pointer",
padding: 0,
marginBottom: "1rem",
fontFamily: "var(--font-sans)",
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M19 12H5M12 5l-7 7 7 7" />
</svg>
العودة إلى قائمة السائقين
</button>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: "1rem" }}>
{/* Avatar + name */}
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<div
style={{
width: 72,
height: 72,
borderRadius: "50%",
overflow: "hidden",
flexShrink: 0,
border: "2px solid var(--color-brand-200)",
background: "var(--color-surface-muted)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{driver.photoUrl && !avatarError ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={driver.photoUrl}
alt={driver.name}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
onError={() => setAvatarError(true)}
/>
) : (
<span style={{ fontSize: 28, fontWeight: 700, color: "var(--color-brand-600)" }}>
{driver.name.charAt(0)}
</span>
)}
</div>
<div>
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600, margin: 0 }}>
ملف السائق
</p>
<h1 style={{ fontSize: "1.5rem", fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
{driver.name}
</h1>
<div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 6, flexWrap: "wrap" }}>
{driver.userName && (
<span style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--color-text-muted)" }}>
@{driver.userName}
</span>
)}
<span style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--color-text-muted)" }}>
{driver.phone}
</span>
{statusConfig && (
<span
style={{
borderRadius: "var(--radius-full)",
border: `1px solid ${statusConfig.border}`,
background: statusConfig.bg,
padding: "0.2rem 0.625rem",
fontSize: 11,
fontWeight: 700,
color: statusConfig.color,
display: "inline-flex",
alignItems: "center",
gap: 5,
}}
>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusConfig.dot, flexShrink: 0 }} />
{statusConfig.label}
</span>
)}
</div>
</div>
</div>
{/* Actions */}
<div style={{ display: "flex", gap: "0.75rem" }}>
<button
type="button"
onClick={() => setDeleteOpen(true)}
style={{
height: 40,
padding: "0 1.25rem",
borderRadius: "var(--radius-lg)",
border: "1px solid #FECACA",
background: "#FEF2F2",
fontSize: 13,
fontWeight: 700,
color: "#DC2626",
cursor: "pointer",
fontFamily: "var(--font-sans)",
}}
>
حذف السائق
</button>
<button
type="button"
onClick={() => setEditOpen(true)}
style={{
height: 40,
padding: "0 1.25rem",
borderRadius: "var(--radius-lg)",
border: "none",
background: "var(--color-brand-600)",
fontSize: 13,
fontWeight: 700,
color: "#FFF",
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: 8,
fontFamily: "var(--font-sans)",
}}
>
<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>
</div>
</div>
</header>
{/* ── Content grid ── */}
<div
style={{
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "1.5rem",
}}
>
{/* Personal Info */}
<SectionCard title="البيانات الشخصية">
<DetailRow label="الاسم الكامل" value={driver.name} />
<DetailRow label="رقم الجوال" value={driver.phone} mono />
<DetailRow label="البريد الإلكتروني" value={driver.email ?? "—"} />
<DetailRow label="العنوان" value={driver.address ?? "—"} />
<DetailRow label="الجنسية" value={driver.nationality ?? "—"} />
<DetailRow label="الفرع" value={driver.branch?.name ?? "—"} />
<DetailRow label="نوع السائق" value={driver.driverType ?? "—"} />
</SectionCard>
{/* ID & GOSI */}
<SectionCard title="الهوية والتأمينات">
<DetailRow
label="نوع الهوية"
value={driver.nationalIdType ? NATIONAL_ID_TYPE_MAP[driver.nationalIdType] : "—"}
/>
<DetailRow label="رقم الهوية" value={driver.nationalId ?? "—"} mono />
<DetailRow
label="انتهاء الهوية"
value={fmtDate(driver.nationalIdExpiry)}
warn={isExpiringSoon(driver.nationalIdExpiry)}
/>
<DetailRow label="رقم GOSI" value={driver.gosiNumber ?? "—"} mono />
</SectionCard>
{/* License */}
<SectionCard title="بيانات رخصة القيادة">
<DetailRow label="رقم الرخصة" value={driver.licenseNumber ?? "—"} mono />
<DetailRow label="نوع الرخصة" value={driver.licenseType ?? "—"} />
<DetailRow
label="انتهاء الرخصة"
value={fmtDate(driver.licenseExpiry)}
warn={isExpiringSoon(driver.licenseExpiry)}
/>
</SectionCard>
{/* Driver Card */}
<SectionCard title="بطاقة السائق">
<DetailRow label="رقم البطاقة" value={driver.driverCardNumber ?? "—"} mono />
<DetailRow
label="نوع البطاقة"
value={driver.driverCardType ? DRIVER_CARD_TYPE_MAP[driver.driverCardType] : "—"}
/>
<DetailRow
label="انتهاء البطاقة"
value={fmtDate(driver.driverCardExpiry)}
warn={isExpiringSoon(driver.driverCardExpiry)}
/>
</SectionCard>
{/* System Info */}
<SectionCard title="معلومات النظام">
<DetailRow label="اسم المستخدم" value={driver.userName ?? "—"} mono />
<DetailRow label="تاريخ الإضافة" value={fmtDate(driver.createdAt)} />
<DetailRow label="آخر تحديث" value={fmtDate(driver.updatedAt)} />
</SectionCard>
{/* Status History */}
{driver.statusHistory && driver.statusHistory.length > 0 && (
<SectionCard title="سجل الحالات">
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{driver.statusHistory.map((h) => {
const s = DRIVER_STATUS_MAP[h.status] ?? DRIVER_STATUS_MAP.Active;
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>
</SectionCard>
)}
</div>
{/* ── Photos section (full width) ── */}
<SectionCard title="الصور والمستندات">
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "1.25rem" }}>
<PhotoCard url={driver.photoUrl} label="صورة السائق" />
<PhotoCard url={driver.nationalPhotoUrl} label="صورة الهوية" />
<PhotoCard url={driver.driverCardPhotoUrl} label="صورة بطاقة السائق" />
</div>
</SectionCard>
{/* ── Report section (full width) ── */}
<div
style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
boxShadow: "var(--shadow-card)",
padding: "1.5rem",
}}
>
<DriverReportPanel driverId={driver.id} />
</div>
</section>
{/* ── Edit modal ── */}
{editOpen && (
<DriverFormModal
editDriver={driver}
branches={[]}
onClose={() => setEditOpen(false)}
onSubmit={handleEditSubmit}
/>
)}
{/* ── Delete modal ── */}
{deleteOpen && (
<DriverDeleteModal
driver={driver}
deleting={deleting}
onCancel={() => setDeleteOpen(false)}
onConfirm={handleConfirmDelete}
/>
)}
</>
);
}

View File

@@ -7,7 +7,10 @@ export default function DashboardLayout({
children: React.ReactNode;
}) {
return (
<div className="flex min-h-screen" style={{ background: "var(--color-surface-muted)" }}>
<div
className="flex min-h-screen"
style={{ background: "var(--color-surface-muted)" }}
>
<Sidebar />
<div className="flex min-w-0 flex-1 flex-col">
<Topbar />

View File

@@ -1,9 +1,9 @@
"use client";
import { useEffect, useState } from "react";
import { getStoredToken } from "../../lib/auth";
import { get } from "../../services/api";
import { Spinner, Alert } from "../../Components/UI";
import { getStoredToken } from "@/lib/auth";
import { get } from "@/services/api";
import { Spinner, Alert } from "@/Components/UI";
interface Order {
id: string;

View File

@@ -0,0 +1,498 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Spinner } from "@/Components/UI";
import { TripFormModal } from "@/Components/Trip/Tripformmodal";
import { TripDeleteModal } from "@/Components/Trip/Tripdeletemodal";
import { TripReportPanel } from "@/Components/Trip_Report/Tripreportpanel";
import { tripService } from "@/services/trip.service";
import { getStoredToken } from "@/lib/auth";
import type {
Trip,
CreateTripPayload,
UpdateTripPayload,
} from "@/types/trip";
import { TRIP_STATUS_MAP } from "@/types/trip";
// ── Helpers ───────────────────────────────────────────────────────────────────
function fmtDate(iso?: string | null): string {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("ar-SA", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function fmtNumber(n?: number | string | null): string {
if (n == null || n === "") return "—";
return Number(n).toLocaleString("ar-SA");
}
// ── Sub-components ────────────────────────────────────────────────────────────
function SectionCard({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div
style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
overflow: "hidden",
boxShadow: "var(--shadow-card)",
}}
>
<div style={{
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
}}>
<p style={{
fontSize: 11,
letterSpacing: "0.25em",
textTransform: "uppercase",
color: "var(--color-text-hint)",
fontWeight: 700,
margin: 0,
}}>
{title}
</p>
</div>
<div style={{ padding: "1rem 1.5rem" }}>{children}</div>
</div>
);
}
function DetailRow({
label,
value,
mono = false,
}: {
label: string;
value: string;
mono?: 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: "var(--color-text-primary)",
maxWidth: "60%",
textAlign: "left",
wordBreak: "break-word",
}}>
{value}
</span>
</div>
);
}
// ── Stat card (للأعداد) ───────────────────────────────────────────────────────
function StatCard({
label,
value,
color = "var(--color-brand-600)",
bg = "var(--color-brand-50, #EFF6FF)",
}: {
label: string;
value: string;
color?: string;
bg?: string;
}) {
return (
<div style={{
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: bg,
padding: "1rem 1.25rem",
display: "flex",
flexDirection: "column",
gap: 4,
}}>
<span style={{ fontSize: 11, fontWeight: 600, color: "var(--color-text-muted)" }}>
{label}
</span>
<span style={{ fontSize: 22, fontWeight: 700, color, fontFamily: "var(--font-mono)" }}>
{value}
</span>
</div>
);
}
// ── Page ──────────────────────────────────────────────────────────────────────
export default function TripDetailPage() {
const params = useParams();
const router = useRouter();
const tripId = params?.tripId as string;
const [trip, setTrip] = useState<Trip | 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);
// ── Load trip ─────────────────────────────────────────────────────────────
const loadTrip = useCallback(async () => {
if (!tripId) return;
setLoading(true);
setError(null);
try {
const token = getStoredToken();
const res = await tripService.getById(tripId, token);
setTrip((res as unknown as { data: Trip }).data);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "تعذّر تحميل بيانات الرحلة.");
} finally {
setLoading(false);
}
}, [tripId]);
useEffect(() => { loadTrip(); }, [loadTrip]);
// ── Edit submit ───────────────────────────────────────────────────────────
const handleEditSubmit = useCallback(
async (
payload: CreateTripPayload | UpdateTripPayload,
_isNew: boolean,
): Promise<boolean> => {
if (!trip) return false;
try {
const token = getStoredToken();
await tripService.update(trip.id, payload as UpdateTripPayload, token);
await loadTrip();
return true;
} catch {
return false;
}
},
[trip, loadTrip],
);
// ── Delete ────────────────────────────────────────────────────────────────
const handleConfirmDelete = useCallback(async () => {
if (!trip) return;
setDeleting(true);
try {
const token = getStoredToken();
await tripService.delete(trip.id, token);
router.back();
} catch {
setDeleting(false);
setDeleteOpen(false);
}
}, [trip, router]);
const statusConfig = trip ? TRIP_STATUS_MAP[trip.status] : null;
// ── Loading ───────────────────────────────────────────────────────────────
if (loading) {
return (
<div style={{ display: "flex", justifyContent: "center", padding: "4rem" }}>
<Spinner size="lg" />
</div>
);
}
// ── Error ─────────────────────────────────────────────────────────────────
if (error || !trip) {
return (
<div style={{ padding: "2rem", textAlign: "center" }}>
<p style={{ fontSize: 14, color: "var(--color-danger)", marginBottom: "1rem" }}>
{error ?? "الرحلة غير موجودة."}
</p>
<button
type="button"
onClick={() => router.back()}
style={{
height: 38, padding: "0 1.25rem",
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13, fontWeight: 600, cursor: "pointer",
fontFamily: "var(--font-sans)",
}}
>
العودة
</button>
</div>
);
}
return (
<>
<section style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* ── Header ── */}
<header style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
boxShadow: "var(--shadow-card)",
padding: "1.5rem",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
gap: "1rem",
}}>
{/* Back + trip info */}
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<button
type="button"
onClick={() => router.back()}
style={{
width: 38, height: 38, borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
display: "flex", alignItems: "center", justifyContent: "center",
cursor: "pointer", flexShrink: 0,
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
{/* Trip icon */}
<div style={{
width: 52, height: 52, borderRadius: "var(--radius-lg)",
background: "var(--color-brand-50, #EFF6FF)",
border: "1px solid var(--color-brand-200, #BFDBFE)",
display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0,
}}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#2563EB" strokeWidth="1.8">
<rect x="1" y="3" width="15" height="13" rx="2" />
<path d="M16 8h4l3 5v4h-7V8z" />
<circle cx="5.5" cy="18.5" r="2.5" />
<circle cx="18.5" cy="18.5" r="2.5" />
</svg>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<h1 style={{ fontSize: 18, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
{trip.title}
</h1>
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
<span style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--color-text-muted)" }}>
{trip.tripNumber}
</span>
{statusConfig && (
<span style={{
borderRadius: "var(--radius-full)",
border: `1px solid ${statusConfig.border}`,
background: statusConfig.bg,
padding: "0.2rem 0.625rem",
fontSize: 11,
fontWeight: 700,
color: statusConfig.color,
display: "inline-flex",
alignItems: "center",
gap: 5,
}}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusConfig.dot, flexShrink: 0 }} />
{statusConfig.label}
</span>
)}
{trip.isDeleted && (
<span style={{
borderRadius: "var(--radius-full)",
border: "1px solid #FECACA",
background: "#FEF2F2",
padding: "0.2rem 0.625rem",
fontSize: 11, fontWeight: 700, color: "#DC2626",
}}>
محذوفة
</span>
)}
</div>
</div>
</div>
{/* Actions */}
<div style={{ display: "flex", gap: "0.75rem" }}>
<button
type="button"
onClick={() => setDeleteOpen(true)}
style={{
height: 40, padding: "0 1.25rem",
borderRadius: "var(--radius-lg)",
border: "1px solid #FECACA",
background: "#FEF2F2",
fontSize: 13, fontWeight: 700, color: "#DC2626",
cursor: "pointer", fontFamily: "var(--font-sans)",
}}
>
حذف الرحلة
</button>
<button
type="button"
onClick={() => setEditOpen(true)}
style={{
height: 40, padding: "0 1.25rem",
borderRadius: "var(--radius-lg)",
border: "none",
background: "var(--color-brand-600)",
fontSize: 13, fontWeight: 700, color: "#FFF",
cursor: "pointer",
display: "flex", alignItems: "center", gap: 8,
fontFamily: "var(--font-sans)",
}}
>
<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>
</div>
</header>
{/* ── Stats row ── */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "1rem" }}>
<StatCard
label="المجمّع"
value={String(trip.collectedCount)}
color="#1E40AF"
bg="#EFF6FF"
/>
<StatCard
label="المُسلَّم"
value={String(trip.deliveredCount)}
color="#166534"
bg="#DCFCE7"
/>
<StatCard
label="المُرتجع"
value={String(trip.returnedCount)}
color="#92400E"
bg="#FFFBEB"
/>
<StatCard
label="النقد المحصّل"
value={fmtNumber(trip.totalCashCollected)}
color="#581C87"
bg="#FAF5FF"
/>
</div>
{/* ── Content grid ── */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1.5rem" }}>
{/* Basic Info */}
<SectionCard title="بيانات الرحلة">
<DetailRow label="رقم الرحلة" value={trip.tripNumber} mono />
<DetailRow label="العنوان" value={trip.title} />
<DetailRow label="الحالة" value={statusConfig?.label ?? trip.status} />
<DetailRow label="الفرع" value={trip.branch?.name ?? "—"} />
<DetailRow label="وقت البدء" value={fmtDate(trip.startTime)} />
<DetailRow label="وقت الانتهاء" value={fmtDate(trip.endTime)} />
<DetailRow label="سبب الإنهاء" value={trip.endReason ?? "—"} />
</SectionCard>
{/* Driver */}
<SectionCard title="السائق">
{trip.driver ? (
<>
<DetailRow label="الاسم" value={trip.driver.name} />
<DetailRow label="الجوال" value={trip.driver.phone} mono />
<DetailRow label="البريد" value={trip.driver.email ?? "—"} />
<DetailRow label="رقم الرخصة" value={trip.driver.licenseNumber ?? "—"} mono />
<DetailRow label="رقم البطاقة" value={trip.driver.driverCardNumber ?? "—"} mono />
<DetailRow label="الجنسية" value={trip.driver.nationality ?? "—"} />
</>
) : (
<p style={{ fontSize: 13, color: "var(--color-text-muted)", margin: 0 }}>لا توجد بيانات للسائق.</p>
)}
</SectionCard>
{/* Car */}
<SectionCard title="السيارة">
{trip.car ? (
<>
<DetailRow label="الماركة" value={trip.car.manufacturer} />
<DetailRow label="الموديل" value={trip.car.model} />
<DetailRow label="السنة" value={trip.car.year != null ? String(trip.car.year) : "—"} />
<DetailRow label="اللون" value={trip.car.color ?? "—"} />
<DetailRow label="رقم اللوحة" value={trip.car.plateNumber} mono />
<DetailRow label="حروف اللوحة" value={trip.car.plateLetters ?? "—"} mono />
<DetailRow label="نوع اللوحة" value={trip.car.plateType ?? "—"} />
<DetailRow label="رقم التسجيل" value={trip.car.registrationNumber ?? "—"} mono />
</>
) : (
<p style={{ fontSize: 13, color: "var(--color-text-muted)", margin: 0 }}>لا توجد بيانات للسيارة.</p>
)}
</SectionCard>
{/* System Info */}
<SectionCard title="معلومات النظام">
<DetailRow label="تاريخ الإضافة" value={fmtDate(trip.createdAt)} />
<DetailRow label="آخر تحديث" value={fmtDate(trip.updatedAt)} />
{trip.isDeleted && (
<DetailRow label="تاريخ الحذف" value={fmtDate(trip.deletedAt)} />
)}
</SectionCard>
</div>
{/* ── Notes (full width, if any) ── */}
{trip.notes && (
<SectionCard title="الملاحظات">
<p style={{ fontSize: 13, color: "var(--color-text-primary)", lineHeight: 1.7, margin: 0 }}>
{trip.notes}
</p>
</SectionCard>
)}
{/* ── Report section ── */}
<div style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
boxShadow: "var(--shadow-card)",
padding: "1.5rem",
}}>
<TripReportPanel tripId={trip.id} />
</div>
</section>
{/* ── Edit modal ── */}
{editOpen && (
<TripFormModal
editTrip={trip}
onClose={() => setEditOpen(false)}
onSubmit={handleEditSubmit}
/>
)}
{/* ── Delete modal ── */}
{deleteOpen && (
<TripDeleteModal
trip={trip}
deleting={deleting}
onCancel={() => setDeleteOpen(false)}
onConfirm={handleConfirmDelete}
/>
)}
</>
);
}

View File

@@ -1,14 +1,14 @@
"use client";
import { useState } from "react";
import { Alert } from "../../Components/UI";
import { UserTable } from "../../Components/User/UserTable";
import { UserFormModal } from "../../Components/User/UserFormModal";
import { UserDetailModal } from "../../Components/User/UserDetailModal";
import { DeleteConfirmModal } from "../../Components/User/DeleteConfirmModal";
import { Toast } from "../../Components/User/Toast";
import { useUsers } from "../../hooks/useUser";
import type { User, UserFormData } from "../../types/user";
import { Alert } from "@/Components/UI";
import { UserTable } from "@/Components/User/UserTable";
import { UserFormModal } from "@/Components/User/UserFormModal";
import { UserDetailModal } from "@/Components/User/UserDetailModal";
import { DeleteConfirmModal } from "@/Components/User/DeleteConfirmModal";
import { Toast } from "@/Components/User/Toast";
import { useUsers } from "@/hooks/useUser";
import type { User, UserFormData } from "@/types/user";
export default function UsersPage() {
// ── Modal state ─────────────────────────────────────────────────────────────

7
app/not_found/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
import React from 'react'
export default function page() {
return (
<div>page</div>
)
}

View File

@@ -1,5 +1,6 @@
import Link from "next/link";
import Navbar from "./components/layout/Navbar";
import Logo from "@/utils/logo";
const stats = [
{ label: "الطلبات النشطة", value: "48", color: "text-blue-600" },
@@ -33,27 +34,57 @@ const roles = [
tone: "dark",
tag: "مشرف",
title: "تحكم كامل",
items: ["عرض جميع طلبات العملاء", "تحديث حالات الطلبات", "إدارة العملاء والسائقين", "سجل التدقيق", "تعيين الرحلات"],
items: [
"عرض جميع طلبات العملاء",
"تحديث حالات الطلبات",
"إدارة العملاء والسائقين",
"سجل التدقيق",
"تعيين الرحلات",
],
},
{
tone: "blue",
tag: "عميل",
title: "طلباتي",
items: ["إضافة طلبات جديدة", "تتبع الشحنات النشطة", "عرض سجل الطلبات", "إدارة العناوين", "التواصل مع الدعم"],
items: [
"إضافة طلبات جديدة",
"تتبع الشحنات النشطة",
"عرض سجل الطلبات",
"إدارة العناوين",
"التواصل مع الدعم",
],
},
{
tone: "green",
tag: "مستخدم",
title: "وصول ميداني",
items: ["عرض الرحلات المعينة", "تحديث حالة التسليم", "الملف الشخصي", "إعدادات الإشعارات", "تفضيلات التطبيق"],
items: [
"عرض الرحلات المعينة",
"تحديث حالة التسليم",
"الملف الشخصي",
"إعدادات الإشعارات",
"تفضيلات التطبيق",
],
},
];
const faqs = [
{ q: "كيف أنشئ حساب عميل؟", a: "اضغط على “إنشاء حساب عميل” ثم أكمل بياناتك الأساسية لتفعيل الحساب في دقائق." },
{ q: "ماذا يحدث إن أدخلت كلمة مرور خاطئة؟", a: "ستظهر لك رسالة خطأ واضحة مع خيار إعادة المحاولة أو استرجاع كلمة المرور." },
{ q: "هل بياناتي الشخصية محمية؟", a: "نعم، يتم تشفير البيانات باستخدام AES-256-GCM وإجراءات أمان متقدمة." },
{ q: "هل يمكن للعميل رؤية طلبات عملاء آخرين؟", a: "لا، كل عميل يرى فقط طلباته الخاصة ضمن لوحة التحكم المخصصة له." },
{
q: "كيف أنشئ حساب عميل؟",
a: "اضغط على “إنشاء حساب عميل” ثم أكمل بياناتك الأساسية لتفعيل الحساب في دقائق.",
},
{
q: "ماذا يحدث إن أدخلت كلمة مرور خاطئة؟",
a: "ستظهر لك رسالة خطأ واضحة مع خيار إعادة المحاولة أو استرجاع كلمة المرور.",
},
{
q: "هل بياناتي الشخصية محمية؟",
a: "نعم، يتم تشفير البيانات باستخدام AES-256-GCM وإجراءات أمان متقدمة.",
},
{
q: "هل يمكن للعميل رؤية طلبات عملاء آخرين؟",
a: "لا، كل عميل يرى فقط طلباته الخاصة ضمن لوحة التحكم المخصصة له.",
},
];
export default function Home() {
@@ -73,29 +104,53 @@ export default function Home() {
إدارة لوجستية سهلة، توصيل مضمون في كل مرة.
</h1>
<p className="max-w-xl text-[14px] leading-7 text-white/75">
أدِر كل شحنة وعميل وسائق من منصة واحدة آمنة. مصممة للمشرفين والعملاء والمستخدمين الميدانيين.
أدِر كل شحنة وعميل وسائق من منصة واحدة آمنة. مصممة للمشرفين
والعملاء والمستخدمين الميدانيين.
</p>
<div className="flex flex-wrap gap-3">
<Link href="/NewOrder" className="h-11 cursor-pointer rounded-xl pt-3 bg-white px-5 text-[13px] font-semibold text-blue-600 shadow-md">
إنشاء طلب جديد
</Link>
<Link href="/login" className="h-11 pt-3 rounded-xl border border-white/30 bg-white/15 px-5 text-[13px] font-semibold text-white shadow-md backdrop-blur">
<Link
href="/login"
className="h-11 pt-3 rounded-xl border border-white/30 bg-white/15 px-5 text-[13px] font-semibold text-white shadow-md backdrop-blur"
>
تسجيل الدخول للوحة التحكم
</Link>
</div>
</article>
<aside className="w-full max-w-md rounded-2xl border border-white/20 bg-white/12 p-5 shadow-2xl shadow-blue-950/30 backdrop-blur-md">
<p className="text-[11px] uppercase tracking-[0.35em] text-white/60">تغذية الطلبات المباشرة</p>
<p className="text-[11px] uppercase tracking-[0.35em] text-white/60">
تغذية الطلبات المباشرة
</p>
<div className="mt-5 space-y-3">
{[
["SHIP-202606-00142", "في الطريق", "bg-amber-100 text-amber-800"],
["SHIP-202606-00139", "تم التسليم", "bg-emerald-100 text-emerald-800"],
["SHIP-202606-00145", "تم الإنشاء", "bg-blue-100 text-blue-800"],
[
"SHIP-202606-00142",
"في الطريق",
"bg-amber-100 text-amber-800",
],
[
"SHIP-202606-00139",
"تم التسليم",
"bg-emerald-100 text-emerald-800",
],
[
"SHIP-202606-00145",
"تم الإنشاء",
"bg-blue-100 text-blue-800",
],
].map(([code, status, badge]) => (
<div key={code} className="flex items-center justify-between rounded-xl border border-white/10 bg-white/8 px-4 py-3">
<span className="font-mono text-[12px] text-white/90">{code}</span>
<span className={`rounded-full px-3 py-1 text-[11px] font-semibold ${badge}`}>{status}</span>
<div
key={code}
className="flex items-center justify-between rounded-xl border border-white/10 bg-white/8 px-4 py-3"
>
<span className="font-mono text-[12px] text-white/90">
{code}
</span>
<span
className={`rounded-full px-3 py-1 text-[11px] font-semibold ${badge}`}
>
{status}
</span>
</div>
))}
</div>
@@ -110,25 +165,94 @@ export default function Home() {
<section className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-6 py-10 lg:px-8">
<div className="grid gap-4 md:grid-cols-3">
{stats.map((item) => (
<article key={item.label} className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<p className="text-[11px] uppercase tracking-[0.35em] text-slate-500">{item.label}</p>
<p className={`mt-3 text-[32px] font-extrabold ${item.color}`}>{item.value}</p>
<article
key={item.label}
className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"
>
<p className="text-[11px] uppercase tracking-[0.35em] text-slate-500">
{item.label}
</p>
<p className={`mt-3 text-[32px] font-extrabold ${item.color}`}>
{item.value}
</p>
</article>
))}
</div>
<article className="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-slate-200 lg:p-8">
<p className="text-[11px] uppercase tracking-[0.35em] text-blue-600">المميزات الأساسية</p>
<h2 className="mt-2 text-[22px] font-extrabold text-slate-900">كل ما يحتاجه فريق اللوجستيك</h2>
<p className="mt-2 text-[14px] text-slate-500">من إدارة الطلبات إلى التتبع المباشر، كل شيء في مكان واحد.</p>
<p className="text-[11px] uppercase tracking-[0.35em] text-blue-600">
المميزات الأساسية
</p>
<h2 className="mt-2 text-[22px] font-extrabold text-slate-900">
كل ما يحتاجه فريق اللوجستيك
</h2>
<p className="mt-2 text-[14px] text-slate-500">
من إدارة الطلبات إلى التتبع المباشر، كل شيء في مكان واحد.
</p>
<div className="mt-6 grid gap-4 md:grid-cols-3">
{features.map((feature) => (
<article key={feature.title} className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<div className={`mb-4 flex h-11 w-11 items-center justify-center rounded-xl ${feature.tone === "blue" ? "bg-blue-100 text-blue-600" : feature.tone === "emerald" ? "bg-emerald-100 text-emerald-600" : "bg-amber-100 text-amber-600"}`}>
{feature.icon === "truck" ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5"><path d="M3 7h11l3 4h4l-2 4H8" /><circle cx="9" cy="18" r="1.7" fill="currentColor" stroke="none" /><circle cx="18" cy="18" r="1.7" fill="currentColor" stroke="none" /></svg> : feature.icon === "clock" ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5"><circle cx="12" cy="12" r="8" /><path d="M12 7v5l3 2" /></svg> : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5"><path d="M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2" /><circle cx="9.5" cy="7" r="3" /><path d="M17 8l2 2 4-4" /></svg>}
<article
key={feature.title}
className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"
>
<div
className={`mb-4 flex h-11 w-11 items-center justify-center rounded-xl ${feature.tone === "blue" ? "bg-blue-100 text-blue-600" : feature.tone === "emerald" ? "bg-emerald-100 text-emerald-600" : "bg-amber-100 text-amber-600"}`}
>
{feature.icon === "truck" ? (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
className="h-5 w-5"
>
<path d="M3 7h11l3 4h4l-2 4H8" />
<circle
cx="9"
cy="18"
r="1.7"
fill="currentColor"
stroke="none"
/>
<circle
cx="18"
cy="18"
r="1.7"
fill="currentColor"
stroke="none"
/>
</svg>
) : feature.icon === "clock" ? (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
className="h-5 w-5"
>
<circle cx="12" cy="12" r="8" />
<path d="M12 7v5l3 2" />
</svg>
) : (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
className="h-5 w-5"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2" />
<circle cx="9.5" cy="7" r="3" />
<path d="M17 8l2 2 4-4" />
</svg>
)}
</div>
<h3 className="text-[16px] font-bold text-slate-900">{feature.title}</h3>
<p className="mt-2 text-[13px] leading-6 text-slate-500">{feature.text}</p>
<h3 className="text-[16px] font-bold text-slate-900">
{feature.title}
</h3>
<p className="mt-2 text-[13px] leading-6 text-slate-500">
{feature.text}
</p>
</article>
))}
</div>
@@ -138,16 +262,35 @@ export default function Home() {
<section className="bg-[#ECEEF2] py-2">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-6 py-10 lg:px-8">
<article className="text-center">
<p className="text-[11px] uppercase tracking-[0.35em] text-blue-600">أدوار المستخدمين</p>
<h2 className="mt-2 text-[22px] font-extrabold text-slate-900">منصة واحدة، ثلاثة مداخل</h2>
<p className="text-[11px] uppercase tracking-[0.35em] text-blue-600">
أدوار المستخدمين
</p>
<h2 className="mt-2 text-[22px] font-extrabold text-slate-900">
منصة واحدة، ثلاثة مداخل
</h2>
</article>
<div className="grid gap-4 md:grid-cols-3">
{roles.map((role) => (
<article key={role.title} className={`rounded-2xl border p-5 shadow-sm ${role.tone === "dark" ? "border-slate-800 bg-slate-950 text-slate-100" : role.tone === "blue" ? "border-blue-200 bg-blue-50 text-blue-950" : "border-emerald-200 bg-emerald-50 text-emerald-950"}`}>
<span className={`inline-flex rounded-full px-3 py-1 text-[11px] font-semibold ${role.tone === "dark" ? "bg-slate-800 text-slate-200" : role.tone === "blue" ? "bg-blue-100 text-blue-700" : "bg-emerald-100 text-emerald-700"}`}>{role.tag}</span>
<h3 className="mt-3 text-[18px] font-extrabold">{role.title}</h3>
<article
key={role.title}
className={`rounded-2xl border p-5 shadow-sm ${role.tone === "dark" ? "border-slate-800 bg-slate-950 text-slate-800" : role.tone === "blue" ? "border-blue-200 bg-blue-50 text-blue-950" : "border-emerald-200 bg-emerald-50 text-emerald-950"}`}
>
<span
className={`inline-flex rounded-full px-3 py-1 text-[11px] font-semibold ${role.tone === "dark" ? "bg-slate-800 text-slate-200" : role.tone === "blue" ? "bg-blue-100 text-blue-700" : "bg-emerald-100 text-emerald-700"}`}
>
{role.tag}
</span>
<h3 className="mt-3 text-[18px] font-extrabold">
{role.title}
</h3>
<ul className="mt-4 space-y-2 text-[13px] leading-6">
{role.items.map((item) => <li key={item} className="flex items-start gap-2"> <span className="mt-1 text-[12px]"></span><span>{item}</span></li>)}
{role.items.map((item) => (
<li key={item} className="flex items-start gap-2">
{" "}
<span className="mt-1 text-[12px]"></span>
<span>{item}</span>
</li>
))}
</ul>
</article>
))}
@@ -157,13 +300,24 @@ export default function Home() {
<section className="mx-auto w-full max-w-7xl px-6 py-10 lg:px-8">
<article className="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-slate-200 lg:p-8">
<p className="text-[11px] uppercase tracking-[0.35em] text-blue-600">الأسئلة الشائعة</p>
<h2 className="mt-2 text-[22px] font-extrabold text-slate-900">أسئلة متكررة</h2>
<p className="text-[11px] uppercase tracking-[0.35em] text-blue-600">
الأسئلة الشائعة
</p>
<h2 className="mt-2 text-[22px] font-extrabold text-slate-900">
أسئلة متكررة
</h2>
<div className="mt-6 space-y-3">
{faqs.map((faq) => (
<article key={faq.q} className="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
<h3 className="text-[14px] font-bold text-slate-900">{faq.q}</h3>
<p className="mt-2 text-[13px] leading-6 text-slate-500">{faq.a}</p>
<article
key={faq.q}
className="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm"
>
<h3 className="text-[14px] font-bold text-slate-900">
{faq.q}
</h3>
<p className="mt-2 text-[13px] leading-6 text-slate-500">
{faq.a}
</p>
</article>
))}
</div>
@@ -172,9 +326,13 @@ export default function Home() {
<footer className="bg-[#0F172A] text-white">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-6 py-6 text-[13px] lg:flex-row lg:items-center lg:justify-between lg:px-8">
<div className="text-right font-extrabold text-white">Slash.so</div>
<div className="text-center text-white/40">© 2026 Slash.so. جميع الحقوق محفوظة.</div>
<div className="flex items-center gap-4 text-white/40">الخصوصية · الشروط · الحالة</div>
<Logo white={true} />
<div className="text-center text-white/40">
© 2026 Slash.sa. جميع الحقوق محفوظة.
</div>
<div className="flex items-center gap-4 text-white/40">
الخصوصية · الشروط · الحالة
</div>
</div>
</footer>
</main>

View File

@@ -1,146 +0,0 @@
"use client";
import Link from "next/link";
import { useMemo, useState } from "react";
export default function RegisterPage() {
const [step, setStep] = useState(1);
const [form, setForm] = useState({ fullName: "", email: "", phone: "", company: "", address: "" });
const [errors, setErrors] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const phonePattern = /^\+?[0-9]{10,15}$/;
function validateStep1() {
const nextErrors: Record<string, string> = {};
if (!form.fullName.trim()) nextErrors.fullName = "This field is required.";
if (!form.email.trim()) nextErrors.email = "This field is required.";
else if (!emailPattern.test(form.email)) nextErrors.email = "Please enter a valid email address (e.g. name@domain.com).";
if (!form.phone.trim()) nextErrors.phone = "This field is required.";
else if (!phonePattern.test(form.phone)) nextErrors.phone = "Phone number must be 1015 digits, with optional country code.";
setErrors(nextErrors);
return Object.keys(nextErrors).length === 0;
}
function handleNext() {
if (validateStep1()) setStep(2);
}
function handleSubmit() {
setLoading(true);
setTimeout(() => {
setLoading(false);
window.location.href = "/client";
}, 600);
}
const progressSteps: Array<[number, string, boolean]> = [
[1, "بياناتك", step >= 1],
[2, "التفضيلات", step >= 2],
[3, "التأكيد", step >= 3],
];
const summaryRows = useMemo(
() => [
["Full Name", form.fullName || "—"],
["Email", form.email || "—"],
["Phone", form.phone || "—"],
["Company", form.company || "—"],
["Address", form.address || "—"],
],
[form],
);
return (
<main className="min-h-screen bg-slate-50 p-6 text-slate-900 md:p-10">
<section className="mx-auto flex w-full max-w-5xl flex-col gap-6">
<header className="flex items-center justify-between gap-3">
<div>
<p className="text-[12px] uppercase tracking-[0.35em] text-blue-600">تسجيل العميل</p>
<h1 className="mt-2 text-[28px] font-bold text-slate-900">إنشاء حساب LogiFlow</h1>
</div>
<Link href="/login" className="rounded-[9px] border border-slate-200 bg-white px-4 py-2 text-sm text-slate-500 hover:bg-slate-100">العودة لتسجيل الدخول</Link>
</header>
<div className="flex items-center gap-3 rounded-[14px] border border-slate-200 bg-white p-4 shadow-sm">
{progressSteps.map(([index, label, done]) => (
<div key={String(index)} className="flex flex-1 items-center gap-3">
<div className={`flex h-8 w-8 items-center justify-center rounded-full border text-[12px] font-bold ${step === index ? "border-blue-600 bg-blue-50 text-blue-600" : done ? "border-emerald-500 bg-emerald-50 text-emerald-600" : "border-slate-200 bg-slate-100 text-slate-400"}`}>
{index}
</div>
<span className={`text-[12px] font-semibold ${step === index ? "text-blue-600" : done ? "text-emerald-600" : "text-slate-500"}`}>{label}</span>
{index < 3 ? <span className="hidden h-px flex-1 bg-slate-200 md:block" /> : null}
</div>
))}
</div>
<article className="rounded-[14px] border border-slate-200 bg-white p-6 shadow-sm md:p-8">
{step === 1 ? (
<div className="space-y-4">
<div>
<h2 className="text-[18px] font-semibold">المعلومات الشخصية</h2>
<p className="text-[13px] text-slate-500">أكمل الحقول المطلوبة للانتقال إلى خطوة التأكيد.</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className="text-[12px] font-semibold uppercase tracking-[0.5px] text-slate-500">الاسم الكامل
<input value={form.fullName} onChange={(e) => setForm({ ...form, fullName: e.target.value })} className="mt-2 h-11 w-full rounded-[9px] border border-slate-200 px-3 text-[14px] text-slate-900" />
{errors.fullName ? <span className="mt-1 block text-[12px] text-red-600">{errors.fullName}</span> : null}
</label>
<label className="text-[12px] font-semibold uppercase tracking-[0.5px] text-slate-500">البريد الإلكتروني
<input value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} className="mt-2 h-11 w-full rounded-[9px] border border-slate-200 px-3 text-[14px] text-slate-900" />
{errors.email ? <span className="mt-1 block text-[12px] text-red-600">{errors.email}</span> : null}
</label>
<label className="text-[12px] font-semibold uppercase tracking-[0.5px] text-slate-500">رقم الهاتف
<input value={form.phone} onChange={(e) => setForm({ ...form, phone: e.target.value })} className="mt-2 h-11 w-full rounded-[9px] border border-slate-200 px-3 text-[14px] text-slate-900" />
{errors.phone ? <span className="mt-1 block text-[12px] text-red-600">{errors.phone}</span> : null}
</label>
<label className="text-[12px] font-semibold uppercase tracking-[0.5px] text-slate-500">اسم الشركة
<input value={form.company} onChange={(e) => setForm({ ...form, company: e.target.value })} className="mt-2 h-11 w-full rounded-[9px] border border-slate-200 px-3 text-[14px] text-slate-900" />
</label>
</div>
<label className="block text-[12px] font-semibold uppercase tracking-[0.5px] text-slate-500">العنوان
<textarea value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} rows={4} className="mt-2 w-full rounded-[9px] border border-slate-200 px-3 py-2 text-[14px] text-slate-900" />
</label>
<div className="flex gap-3 pt-2">
<Link href="/login" className="flex h-10 flex-1 items-center justify-center rounded-[9px] border border-slate-200 bg-white text-sm font-semibold text-slate-500">رجوع</Link>
<button type="button" onClick={handleNext} className="flex h-10 flex-2 items-center justify-center rounded-[9px] bg-blue-600 text-sm font-semibold text-white hover:bg-blue-700">التالي</button>
</div>
</div>
) : null}
{step === 2 ? (
<div className="space-y-4">
<div>
<h2 className="text-[18px] font-semibold">التفضيلات</h2>
<p className="text-[13px] text-slate-500">راجع التفاصيل الاختيارية وانتقل إلى المراجعة.</p>
</div>
<div className="rounded-[10px] border border-slate-200 bg-slate-100 p-4 text-sm text-slate-500">سيتم إنشاء الحساب بشكل آمن. يتم تشفير البيانات الحساسة أثناء التخزين.</div>
<div className="flex gap-3 pt-2">
<button type="button" onClick={() => setStep(1)} className="flex h-10 flex-1 items-center justify-center rounded-[9px] border border-slate-200 bg-white text-sm font-semibold text-slate-500">رجوع</button>
<button type="button" onClick={() => setStep(3)} className="flex h-10 flex-2 items-center justify-center rounded-[9px] bg-blue-600 text-sm font-semibold text-white hover:bg-blue-700">مراجعة</button>
</div>
</div>
) : null}
{step === 3 ? (
<div className="space-y-4">
<div>
<h2 className="text-[18px] font-semibold">تأكيد وإرسال</h2>
<p className="text-[13px] text-slate-500">راجع بياناتك قبل إنشاء الحساب.</p>
</div>
<table className="w-full overflow-hidden rounded-[10px] border border-slate-200 text-left text-sm">
<tbody>{summaryRows.map(([label, value]) => <tr key={label} className="border-b border-slate-200 last:border-b-0"><td className="bg-slate-100 px-4 py-3 font-semibold text-slate-500">{label}</td><td className="px-4 py-3 text-slate-900">{value}</td></tr>)}</tbody>
</table>
<div className="rounded-[10px] border border-blue-200 bg-blue-50 p-3 text-sm text-slate-900">سيتم إنشاء الحساب بشكل آمن. يتم تشفير البيانات الحساسة أثناء التخزين.</div>
<div className="flex gap-3 pt-2">
<button type="button" onClick={() => setStep(2)} className="flex h-10 flex-1 items-center justify-center rounded-[9px] border border-slate-200 bg-white text-sm font-semibold text-slate-500">رجوع</button>
<button type="button" disabled={loading} onClick={handleSubmit} className="flex h-10 flex-2 items-center justify-center rounded-[9px] bg-emerald-500 text-sm font-semibold text-white hover:bg-emerald-600 disabled:cursor-not-allowed disabled:opacity-70">{loading ? "جاري إنشاء الحساب…" : "إنشاء حسابي"}</button>
</div>
</div>
) : null}
</article>
</section>
</main>
);
}

View File

@@ -1,429 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { PageLoader } from "../../../Components/UI/Spinner";
import { Alert } from "../../../Components/UI/Alert";
import { fmtDate } from "../../../utils/helperFun";
import { getStoredToken } from "../../../lib/auth";
import { userService } from "../../../services/user.service";
import type { UserDetail } from "../../../types/user";
// ── helpers ───────────────────────────────────────────────────────────────────
function safeFmtDate(iso: string | null | undefined): string {
if (!iso) return "—";
try {
const d = new Date(iso);
if (isNaN(d.getTime())) return "—";
return fmtDate(iso);
} catch {
return "—";
}
}
// ── sub-components ────────────────────────────────────────────────────────────
function SectionCard({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div style={{
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-xl)",
boxShadow: "var(--shadow-card)",
overflow: "hidden",
}}>
<div style={{
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
}}>
<p style={{
margin: 0,
fontSize: 11,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.2em",
color: "var(--color-text-muted)",
}}>
{title}
</p>
</div>
<div style={{ padding: "1.25rem 1.5rem" }}>
{children}
</div>
</div>
);
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={{
fontSize: 11,
fontWeight: 600,
color: "var(--color-text-muted)",
textTransform: "uppercase",
letterSpacing: "0.15em",
}}>
{label}
</span>
<span style={{
fontSize: 13,
color: "var(--color-text-primary)",
fontFamily: "var(--font-sans)",
}}>
{children}
</span>
</div>
);
}
function ActiveBadge({ active }: { active: boolean }) {
return (
<span style={{
display: "inline-flex", alignItems: "center", gap: 5,
borderRadius: "var(--radius-full)",
border: active ? "1px solid #BBF7D0" : "1px solid #FECACA",
background: active ? "#DCFCE7" : "#FEF2F2",
padding: "0.2rem 0.75rem",
fontSize: 12, fontWeight: 600,
color: active ? "#166534" : "#991B1B",
}}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: active ? "#16A34A" : "#DC2626" }} />
{active ? "نشط" : "معطل"}
</span>
);
}
function DeletedBadge({ deleted }: { deleted: boolean }) {
if (!deleted) return (
<span style={{
display: "inline-flex", alignItems: "center", gap: 5,
borderRadius: "var(--radius-full)",
border: "1px solid #BBF7D0",
background: "#DCFCE7",
padding: "0.2rem 0.75rem",
fontSize: 12, fontWeight: 600,
color: "#166534",
}}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "#16A34A" }} />
موجود
</span>
);
return (
<span style={{
display: "inline-flex", alignItems: "center", gap: 5,
borderRadius: "var(--radius-full)",
border: "1px solid #FECACA",
background: "#FEF2F2",
padding: "0.2rem 0.75rem",
fontSize: 12, fontWeight: 600,
color: "#991B1B",
}}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "#DC2626" }} />
محذوف
</span>
);
}
// eye icon
function EyeIcon({ open }: { open: boolean }) {
if (open) return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94" />
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19" />
<line x1="1" y1="1" x2="23" y2="23" />
</svg>
);
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
}
// silhouette fallback avatar
function AvatarFallback() {
return (
<div style={{
width: 80, height: 80, borderRadius: "50%",
background: "var(--color-brand-100)",
border: "2px solid var(--color-brand-200)",
display: "flex", alignItems: "center", justifyContent: "center",
flexShrink: 0,
}}>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#2563EB" strokeWidth="1.5">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</div>
);
}
// ── main page ─────────────────────────────────────────────────────────────────
export default function UserDetailPage() {
const params = useParams();
const router = useRouter();
const userId = typeof params?.userId === "string" ? params.userId : null;
const [user, setUser] = useState<UserDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showToken, setShowToken] = useState(false);
useEffect(() => {
if (!userId) {
router.replace("/users");
return;
}
let cancelled = false;
async function fetchUser() {
try {
const token = getStoredToken();
const res = await userService.getById(userId as string, token);
if (!cancelled) {
setUser((res as { data: UserDetail }).data ?? null);
}
} catch {
if (!cancelled) setError("تعذّر تحميل بيانات المستخدم. يرجى المحاولة لاحقاً.");
} finally {
if (!cancelled) setLoading(false);
}
}
fetchUser();
return () => { cancelled = true; };
}, [userId, router]);
// reset token visibility on unmount
useEffect(() => () => { setShowToken(false); }, []);
if (loading) return <PageLoader message="جارٍ تحميل بيانات المستخدم…" />;
if (error || !user) {
return (
<div dir="rtl" style={{ padding: "2rem", maxWidth: 560 }}>
<Alert
type="error"
title="خطأ في التحميل"
message={error ?? "المستخدم غير موجود."}
/>
<a
href="/users"
style={{
display: "inline-block", marginTop: "1rem",
fontSize: 13, fontWeight: 600,
color: "var(--color-brand-600)",
textDecoration: "underline",
}}
>
العودة إلى قائمة المستخدمين
</a>
</div>
);
}
return (
<div dir="rtl" style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* soft-deleted warning banner */}
{user.isDeleted && (
<div style={{
display: "flex", alignItems: "center", gap: 10,
background: "#FEF2F2", border: "1px solid #FECACA",
borderRadius: "var(--radius-lg)", padding: "0.875rem 1.25rem",
fontSize: 13, fontWeight: 600, color: "#991B1B",
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
هذا المستخدم محذوف تم حذفه بتاريخ {safeFmtDate(user.deletedAt)}.
</div>
)}
{/* page header */}
<header style={{
background: "var(--color-surface)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-xl)",
boxShadow: "var(--shadow-card)",
padding: "1.5rem 2rem",
}}>
<a
href="/users"
style={{
display: "inline-flex", alignItems: "center", gap: 6,
fontSize: 12, fontWeight: 600,
color: "var(--color-brand-600)",
textDecoration: "none", marginBottom: "1rem",
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<polyline points="15 18 9 12 15 6" />
</svg>
العودة إلى المستخدمين
</a>
<div style={{ display: "flex", alignItems: "center", gap: "1.25rem", flexWrap: "wrap" }}>
{/* avatar */}
{user.photo
? <img src={user.photo} alt={user.name} style={{ width: 80, height: 80, borderRadius: "50%", objectFit: "cover", border: "2px solid var(--color-brand-200)", flexShrink: 0 }} />
: <AvatarFallback />
}
<div>
<p style={{ margin: 0, fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600 }}>
ملف المستخدم
</p>
<h1 style={{ margin: "4px 0 6px", fontSize: "1.4rem", fontWeight: 700, color: "var(--color-text-primary)" }}>
{user.name}
</h1>
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
{user.userName && (
<span style={{
fontFamily: "var(--font-mono)", fontSize: 12,
color: "#2563EB", fontWeight: 600,
background: "#EFF6FF", border: "1px solid #BFDBFE",
borderRadius: "var(--radius-sm)", padding: "2px 8px",
}}>
@{user.userName}
</span>
)}
<ActiveBadge active={user.isActive} />
<DeletedBadge deleted={user.isDeleted} />
</div>
</div>
</div>
</header>
{/* grid layout */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: "1.25rem",
}}>
{/* Basic Info */}
<SectionCard title="المعلومات الأساسية">
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<Field label="الاسم الكامل">{user.name}</Field>
<Field label="البريد الإلكتروني">
{user.email
? <span style={{ direction: "ltr", display: "inline-block" }}>{user.email}</span>
: "—"
}
</Field>
<Field label="رقم الهاتف">
<span style={{ direction: "ltr", display: "inline-block", fontFamily: "var(--font-mono)" }}>
{user.phone || "—"}
</span>
</Field>
<Field label="المعرف">
<span style={{
fontFamily: "var(--font-mono)", fontSize: 11,
color: "var(--color-text-muted)", wordBreak: "break-all",
}}>
{user.id}
</span>
</Field>
</div>
</SectionCard>
{/* Role & Branch */}
<SectionCard title="الدور والفرع">
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<Field label="الدور">
<span style={{
display: "inline-flex", alignItems: "center",
borderRadius: "var(--radius-full)",
border: "1px solid #BFDBFE",
background: "#EFF6FF",
padding: "0.2rem 0.75rem",
fontSize: 12, fontWeight: 600,
color: "#1D4ED8",
}}>
{user.role?.name ?? "—"}
</span>
</Field>
{user.role?.description && (
<Field label="وصف الدور">
<span style={{ color: "var(--color-text-secondary)" }}>
{user.role.description}
</span>
</Field>
)}
<Field label="الفرع">{user.branch?.name ?? "—"}</Field>
</div>
</SectionCard>
{/* Account Status */}
<SectionCard title="حالة الحساب">
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
<Field label="الحالة"><ActiveBadge active={user.isActive} /></Field>
<Field label="حالة الحذف"><DeletedBadge deleted={user.isDeleted} /></Field>
<Field label="تاريخ الإنشاء">{safeFmtDate(user.createdAt)}</Field>
<Field label="آخر تحديث">{safeFmtDate(user.updatedAt)}</Field>
<Field label="تاريخ الحذف">{safeFmtDate(user.deletedAt)}</Field>
<Field label="آخر تغيير لكلمة المرور">{safeFmtDate(user.passwordChangedAt)}</Field>
</div>
</SectionCard>
{/* Refresh Token */}
<SectionCard title="رمز التحديث">
{user.refreshToken ? (
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<div style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
background: "var(--color-surface-muted)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-md)",
padding: "0.625rem 0.875rem",
gap: "0.75rem",
}}>
<span style={{
fontFamily: "var(--font-mono)", fontSize: 12,
color: "var(--color-text-secondary)",
wordBreak: "break-all", flex: 1,
letterSpacing: showToken ? "normal" : "0.15em",
}}>
{showToken ? user.refreshToken : "••••••••••••••••••••••••"}
</span>
<button
type="button"
onClick={() => setShowToken(p => !p)}
title={showToken ? "إخفاء الرمز" : "إظهار الرمز"}
aria-label={showToken ? "إخفاء رمز التحديث" : "إظهار رمز التحديث"}
style={{
flexShrink: 0,
width: 30, height: 30,
borderRadius: "var(--radius-sm)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
color: "var(--color-text-muted)",
cursor: "pointer",
display: "inline-flex", alignItems: "center", justifyContent: "center",
transition: "opacity 150ms",
}}
>
<EyeIcon open={showToken} />
</button>
</div>
<p style={{ margin: 0, fontSize: 11, color: "var(--color-text-hint)" }}>
هذا الرمز حساس لا تشاركه مع أي أحد.
</p>
</div>
) : (
<p style={{
margin: 0, fontSize: 13,
color: "var(--color-text-muted)",
fontStyle: "italic",
}}>
غير متاح
</p>
)}
</SectionCard>
</div>
</div>
);
}