create user pages crud also update pages ui build order and car ,driver first page interfase create model layer to componants

This commit is contained in:
m7amedez5511
2026-06-10 16:58:17 +03:00
parent 53c03e9867
commit e0e38dc87a
34 changed files with 2963 additions and 862 deletions

View File

@@ -1,25 +1,25 @@
"use client";
// ─────────────────────────────────────────────
// page.tsx
// نقطة الدخول — تحقق من الجلسة وتوجيه المستخدم
// ─────────────────────────────────────────────
import { useState, useEffect, useCallback } from "react";
import { loadSession, type ClientSession } from "@/utils/helperFun";
import RegisterStep from "../../Components/Client/client";
import DashboardStep from "../../Components/Order/order";
import Logo from "@/utils/logo";
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" | "dashboard";
type Step = "bootstrap" | "register" | "address" | "dashboard";
/* شريط التنقل */
function Navbar({ session }: { session: ClientSession | null }) {
return (
<nav className="bg-white border-b border-[#E5E7EB] flex items-center justify-between px-5 h-14 sticky top-0 z-30">
<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 className="w-8 h-8 rounded-full bg-[#EBF3FF] flex items-center justify-center">
<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"/>
@@ -31,35 +31,26 @@ function Navbar({ session }: { session: ClientSession | null }) {
);
}
/* شاشة التحميل */
function BootstrapScreen() {
return (
<div className="flex flex-col items-center justify-center py-10 gap-3">
<div className="w-10 h-10 rounded-full border-4 border-[#1A73E8] border-t-transparent animate-spin" />
<p className="text-[13px] text-[#6B7280]">جارٍ تحميل بيانات الجلسة</p>
</div>
);
}
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) {
// user has valid session → dashboard
setSession(saved);
setStep("dashboard");
} else {
// new or invalid session → register
setStep("register");
}
}, []);
const handleRegistered = useCallback((s: ClientSession) => {
setSession(s);
setStep("address");
}, []);
const handleAddressComplete = useCallback(() => {
setStep("dashboard");
}, []);
@@ -68,18 +59,28 @@ export default function ClientOrderPage() {
className="min-h-screen bg-[#ECEEF2]"
dir="rtl"
lang="ar"
style={{ fontFamily: "'Cairo', 'IBM Plex Sans Arabic', Tahoma, sans-serif" }}
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" && <BootstrapScreen />}
{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} />
)}

View File

@@ -1,3 +1,417 @@
export default function CarsPage() {
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Cars module is ready for the fleet management dashboard.</section>;
"use client";
import { useEffect, useState } from "react";
import { getStoredToken } from "../../lib/auth";
import { get } from "../../services/api";
import { Spinner, Alert } from "../../Components/UI";
interface Car {
id: string;
manufacturer: string;
model: string;
year: number;
color?: string;
plateNumber: string;
plateLetters: string;
currentStatus: "Active" | "InMaintenance" | "InTrip" | "Inactive";
insuranceStatus?: "Valid" | "Expired" | "NotInsured";
insuranceExpiryDate?: string;
registrationExpiryDate?: string;
branch?: { name: string };
createdAt: string;
}
interface ApiResponse {
data: {
data: Car[];
pagination: { total: number; page: number; pages: number };
meta?: { total: number; pages: number };
};
}
const STATUS_MAP: Record<
Car["currentStatus"],
{ label: string; color: string; bg: string; border: string }
> = {
Active: { label: "نشط", color: "#166534", bg: "#DCFCE7", border: "#BBF7D0" },
InMaintenance: {
label: "صيانة",
color: "#854D0E",
bg: "#FFFBEB",
border: "#FDE68A",
},
InTrip: {
label: "في رحلة",
color: "#1E40AF",
bg: "#EFF6FF",
border: "#BFDBFE",
},
Inactive: {
label: "غير نشط",
color: "#64748B",
bg: "#F1F5F9",
border: "#E2E8F0",
},
};
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",
});
}
const cardStyle: React.CSSProperties = {
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
overflow: "hidden",
boxShadow: "var(--shadow-card)",
};
const thStyle: React.CSSProperties = {
padding: "0.75rem 1.5rem",
fontSize: 11,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.2em",
color: "var(--color-text-muted)",
background: "var(--color-surface-muted)",
borderBottom: "1px solid var(--color-border)",
};
export default function CarsPage() {
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);
useEffect(() => {
const token = getStoredToken();
setLoading(true);
setError(null);
const query = `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
get<ApiResponse>(`cars${query}`, token)
.then((res) => {
const payload = res.data ?? res;
setCars(payload.data ?? []);
setTotal(payload.meta?.total ?? 0);
setPages(payload.meta?.pages ?? 1);
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [page, search]);
return (
<section
style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}
>
{/* 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)",
}}
>
<p
style={{
fontSize: 11,
letterSpacing: "0.3em",
textTransform: "uppercase",
color: "#2563EB",
fontWeight: 600,
}}
>
إدارة الأسطول
</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,
}}
>
السيارات
</h1>
<p
style={{
marginTop: "0.25rem",
fontSize: 13,
color: "var(--color-text-muted)",
}}
>
إجمالي{" "}
<strong style={{ color: "var(--color-text-primary)" }}>
{total}
</strong>{" "}
مركبة في الأسطول
</p>
</div>
<div style={{ position: "relative", width: "100%", maxWidth: 288 }}>
<svg
style={{
position: "absolute",
right: 12,
top: "50%",
transform: "translateY(-50%)",
width: 16,
height: 16,
color: "var(--color-text-hint)",
pointerEvents: "none",
}}
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) => {
setSearch(e.target.value);
setPage(1);
}}
dir="rtl"
style={{
width: "100%",
height: 40,
paddingRight: 36,
paddingLeft: 12,
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13,
color: "var(--color-text-primary)",
outline: "none",
fontFamily: "var(--font-sans)",
}}
/>
</div>
</div>
</header>
{error && (
<Alert type="error" message={error} onClose={() => setError(null)} />
)}
{/* Table */}
<div style={cardStyle}>
<div
dir="rtl"
style={{
display: "grid",
gridTemplateColumns: "2fr 1.5fr 1fr 1fr 1fr 1fr",
...thStyle,
}}
>
<span>السيارة</span>
<span>اللوحة</span>
<span>الفرع</span>
<span>الحالة</span>
<span>التأمين</span>
<span style={{ textAlign: "center" }}>انتهاء الاستمارة</span>
</div>
{loading ? (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 12,
padding: "4rem 0",
color: "var(--color-text-muted)",
}}
>
<Spinner size="sm" className="text-blue-600" />
<span style={{ fontSize: 13 }}>جارٍ التحميل</span>
</div>
) : cars.length === 0 ? (
<p
style={{
textAlign: "center",
padding: "4rem 0",
fontSize: 13,
color: "var(--color-text-muted)",
}}
>
لا توجد نتائج {search && `لـ "${search}"`}
</p>
) : (
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
{cars.map((c, i) => {
const status = STATUS_MAP[c.currentStatus];
const ins = c.insuranceStatus ? INS_MAP[c.insuranceStatus] : null;
return (
<li
key={c.id}
style={{
display: "grid",
gridTemplateColumns: "2fr 1.5fr 1fr 1fr 1fr 1fr",
alignItems: "center",
gap: "0.5rem",
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background:
i % 2 !== 0
? "var(--color-surface-muted)"
: "transparent",
fontSize: 13,
}}
>
<div>
<p
style={{
fontWeight: 600,
color: "var(--color-text-primary)",
margin: 0,
}}
>
{c.manufacturer} {c.model}
</p>
<p
style={{
marginTop: 2,
fontSize: 11,
color: "var(--color-text-muted)",
}}
>
{c.year}
{c.color ? ` · ${c.color}` : ""}
</p>
</div>
<span
style={{
fontFamily: "var(--font-mono)",
fontSize: 13,
fontWeight: 700,
letterSpacing: "0.08em",
color: "#2563EB",
}}
>
{c.plateLetters} {c.plateNumber}
</span>
<span style={{ color: "var(--color-text-secondary)" }}>
{c.branch?.name ?? "—"}
</span>
<span
style={{
display: "inline-flex",
alignItems: "center",
borderRadius: "var(--radius-full)",
border: `1px solid ${status.border}`,
background: status.bg,
padding: "0.2rem 0.625rem",
fontSize: 11,
fontWeight: 600,
color: status.color,
width: "fit-content",
}}
>
{status.label}
</span>
<span
style={{
fontSize: 12,
fontWeight: 600,
color: ins?.color ?? "var(--color-text-muted)",
}}
>
{ins?.label ?? "—"}
</span>
<span
style={{
textAlign: "center",
fontSize: 11,
color: "var(--color-text-muted)",
}}
>
{fmtDate(c.registrationExpiryDate)}
</span>
</li>
);
})}
</ul>
)}
{pages > 1 && (
<div
dir="rtl"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
borderTop: "1px solid var(--color-border)",
padding: "0.875rem 1.5rem",
}}
>
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
صفحة{" "}
<strong style={{ color: "var(--color-text-primary)" }}>
{page}
</strong>{" "}
من{" "}
<strong style={{ color: "var(--color-text-primary)" }}>
{pages}
</strong>
</span>
<div style={{ display: "flex", gap: "0.5rem" }}>
{[
{
label: "السابق",
action: () => setPage((p) => Math.max(1, p - 1)),
disabled: page === 1,
},
{
label: "التالي",
action: () => setPage((p) => Math.min(pages, p + 1)),
disabled: page === pages,
},
].map((btn) => (
<button
key={btn.label}
onClick={btn.action}
disabled={btn.disabled}
style={{
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
padding: "0.375rem 0.875rem",
fontSize: 12,
color: "var(--color-text-secondary)",
cursor: btn.disabled ? "not-allowed" : "pointer",
opacity: btn.disabled ? 0.4 : 1,
fontFamily: "var(--font-sans)",
}}
>
{btn.label}
</button>
))}
</div>
</div>
)}
</div>
</section>
);
}

View File

@@ -1,74 +1,130 @@
// File: app/components/layout/Sidebar.tsx
"use client";
import Link from "next/link";
// FIX: Import getStoredUser so the sidebar reads the authenticated user's
// name and role from localStorage instead of showing hardcoded placeholders.
import Link from "next/link";
import { usePathname } from "next/navigation";
import { getStoredUser } from "../../../lib/auth";
import Logo from "../../../utils/logo";
const navItems = [
{ href: "/dashboard", label: "Dashboard", permission: "read-dashboard" },
{ href: "/users", label: "Users", permission: "read-user" },
{ href: "/cars", label: "Cars", permission: "read-car" },
{ href: "/drivers", label: "Drivers", permission: "read-driver" },
{ href: "/clients", label: "Clients", permission: "read-client" },
{ href: "/orders", label: "Orders", permission: "read-order" },
{ href: "/trips", label: "Trips", permission: "read-trip" },
{ href: "/branches", label: "Branches", permission: "read-branch" },
{ href: "/roles", label: "Roles", permission: "read-role" },
{ href: "/audit", label: "Audit", permission: "read-audit" },
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" },
],
},
{
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" },
],
},
{
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" },
],
},
];
export function Sidebar() {
// FIX: Read the stored user on render. suppressHydrationWarning handles
// the SSR/CSR mismatch since localStorage is browser-only.
const user = getStoredUser();
const permissions = user?.permissions ?? navItems.map((i) => i.permission);
const pathname = usePathname();
const user = getStoredUser();
const permissions = user?.permissions ?? navSections.flatMap(s => s.items.map(i => i.permission));
return (
<aside
suppressHydrationWarning
className="hidden w-72 shrink-0 border-r border-white/10 bg-slate-950/80 p-6 lg:flex lg:flex-col"
className="hidden lg:flex lg:flex-col"
style={{
width: "var(--sidebar-width)",
flexShrink: 0,
background: "linear-gradient(175deg, #1E3A8A 0%, #1D4ED8 60%, #1565C0 100%)",
minHeight: "100vh",
padding: "20px 12px",
}}
>
<div>
<p className="text-xs uppercase tracking-[0.35em] text-cyan-200">Logistics</p>
<h2 className="mt-3 text-xl font-semibold text-white">Ops Console</h2>
<p className="mt-2 text-sm text-slate-300">
Fleet, clients, orders, and compliance in one place.
</p>
{/* Logo */}
<div style={{ marginBottom: 28 }}>
<Logo white />
</div>
<nav className="mt-8 space-y-2">
{navItems.map((item) => {
// FIX: Check the real user permissions array fetched from stored auth.
// Dashboard is always visible regardless of permissions.
const hasPermission =
item.permission === "read-dashboard" ||
permissions.includes(item.permission);
if (!hasPermission) return null;
{/* Nav sections */}
<nav
style={{ flex: 1, display: "flex", flexDirection: "column", gap: 0 }}
aria-label="Main navigation"
>
{navSections.map((section) => (
<div key={section.label} style={{ marginBottom: 8 }}>
<p style={{
fontSize: 9,
letterSpacing: "0.14em",
textTransform: "uppercase",
color: "rgba(255,255,255,0.45)",
padding: "8px 8px 4px",
fontWeight: 600,
}}>
{section.label}
</p>
return (
<Link
key={item.href}
href={item.href}
className="flex items-center justify-between rounded-2xl border border-white/8 bg-slate-900/70 px-4 py-3 text-sm text-slate-200 transition hover:border-cyan-400/30 hover:bg-slate-800"
>
<span>{item.label}</span>
<span className="text-xs text-slate-400"></span>
</Link>
);
})}
{section.items.map((item) => {
const allowed = item.permission === "read-dashboard"
|| permissions.includes(item.permission);
if (!allowed) return null;
const active = pathname === item.href
|| (item.href !== "/dashboard" && pathname.startsWith(item.href));
return (
<Link
key={item.href}
href={item.href}
style={{
display: "flex",
alignItems: "center",
gap: 9,
padding: "7px 8px",
borderRadius: 8,
fontSize: 13,
fontWeight: active ? 500 : 400,
color: active ? "#FFFFFF" : "rgba(255,255,255,0.55)",
background: active ? "rgba(255,255,255,0.15)" : "transparent",
textDecoration: "none",
transition: "var(--transition-base)",
marginBottom: 2,
}}
>
<i
className={`ti ${item.icon}`}
aria-hidden="true"
style={{ fontSize: 16 }}
/>
{item.label}
</Link>
);
})}
</div>
))}
</nav>
<div className="mt-auto rounded-3xl border border-emerald-400/20 bg-emerald-400/10 p-4 text-sm text-emerald-50">
<p className="text-xs uppercase tracking-[0.25em] text-emerald-100">Authenticated</p>
{/* FIX: Show the real user name and role from the stored auth session,
replacing the previous hardcoded "Operations user" / "Signed in" text. */}
<p suppressHydrationWarning className="mt-2 font-semibold">
{/* User badge */}
<div style={{
background: "rgba(255,255,255,0.12)",
borderRadius: 10,
padding: "10px 12px",
marginTop: 8,
}}>
<p suppressHydrationWarning style={{ fontSize: 13, fontWeight: 500, color: "#fff", margin: 0 }}>
{user?.name ?? user?.userName ?? "—"}
</p>
<p suppressHydrationWarning className="text-xs text-emerald-100/90">
{user?.role ? `Role: ${user.role}` : "Signed in"}
<p suppressHydrationWarning style={{ fontSize: 11, color: "rgba(255,255,255,0.55)", marginTop: 2 }}>
{user?.role ?? "Signed in"}
</p>
</div>
</aside>

View File

@@ -1,68 +1,72 @@
// File: app/components/layout/Topbar.tsx
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
// FIX: Import both clearAuth and getStoredUser so the topbar can show the
// authenticated user's role and handle logout properly.
import { clearAuth, getStoredUser } from "../../../lib/auth";
export function Topbar() {
const router = useRouter();
function handleLogout() {
// FIX: clearAuth already existed and is correct — it removes both the
// token and user from localStorage, fully ending the session.
clearAuth();
router.replace("/login");
}
// FIX: Read the stored user to display their real role in the header
// instead of the hardcoded "Manager" placeholder.
const user = getStoredUser();
// Capitalize the role label for display (e.g. "admin" → "Admin")
const roleLabel = user?.role
? user.role.charAt(0).toUpperCase() + user.role.slice(1)
: "—";
return (
<header
suppressHydrationWarning
className="border-b border-white/10 bg-slate-950/70 px-4 py-4 lg:px-6"
style={{
height: "var(--topbar-height)",
background: "#FFFFFF",
borderBottom: "1px solid var(--color-border)",
padding: "0 24px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4">
<div>
<p className="text-xs uppercase tracking-[0.35em] text-cyan-200">
Operations dashboard
</p>
<h1 className="text-xl font-semibold text-white lg:text-2xl">
Logistics delivery management
</h1>
<div>
<p style={{ fontSize: 14, fontWeight: 600, color: "var(--color-text-primary)", margin: 0 }}>
لوحة التحكم
</p>
<p style={{ fontSize: 11, color: "var(--color-text-muted)", margin: 0 }}>
Operations dashboard
</p>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div
suppressHydrationWarning
style={{
fontSize: 12,
color: "var(--color-text-secondary)",
background: "var(--color-surface-muted)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-full)",
padding: "4px 12px",
}}
>
{user?.role ?? "—"}
</div>
<div className="flex items-center gap-3">
<Link
href="/"
className="rounded-full border border-white/10 bg-slate-900/80 px-4 py-2 text-sm text-slate-200 hover:bg-slate-800"
>
Overview
</Link>
{/* FIX: Replace the hardcoded "Manager" string with the actual
authenticated user's role read from localStorage. */}
<div
suppressHydrationWarning
className="hidden rounded-full border border-white/10 bg-slate-900/80 px-4 py-2 text-sm text-slate-200 md:block"
>
{roleLabel}
</div>
<button
type="button"
onClick={handleLogout}
className="rounded-full bg-rose-500/10 px-4 py-2 text-sm text-rose-100 hover:bg-rose-500/20"
>
Logout
</button>
</div>
<button
type="button"
onClick={handleLogout}
style={{
fontSize: 12,
fontWeight: 500,
color: "#DC2626",
background: "#FEF2F2",
border: "1px solid #FECACA",
borderRadius: "var(--radius-full)",
padding: "5px 14px",
cursor: "pointer",
transition: "var(--transition-base)",
}}
>
تسجيل الخروج
</button>
</div>
</header>
);

View File

@@ -1,16 +1,18 @@
import { Sidebar } from "../components/layout/Sidebar";
import { Topbar } from "../components/layout/Topbar";
import { Topbar } from "../components/layout/Topbar";
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen bg-[radial-gradient(circle_at_top,#0f172a_0%,#111827_45%,#020617_100%)] text-slate-100">
<div className="flex min-h-screen">
<Sidebar />
<div className="flex min-w-0 flex-1 flex-col">
<Topbar />
<main className="flex-1 p-4 lg:p-6">{children}</main>
</div>
<div className="flex min-h-screen" style={{ background: "var(--color-surface-muted)" }}>
<Sidebar />
<div className="flex min-w-0 flex-1 flex-col">
<Topbar />
<main className="flex-1 p-6">{children}</main>
</div>
</div>
);
}
}

View File

@@ -2,160 +2,219 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { getDashboardSummary, type DashboardSummaryResponse } from "../../lib/api";
import { clearAuth, getStoredToken, getStoredUser } from "../../lib/auth";
import { useEffect, useMemo } from "react";
import { useDashboardSummary } from "../../hooks/useDashboardSummary";
import { clearAuth, getStoredUser } from "../../lib/auth";
import { Spinner, Alert } from "../../Components/UI";
export default function DashboardPage() {
const router = useRouter();
const [data, setData] = useState<DashboardSummaryResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const { data, error, loading } = useDashboardSummary();
useEffect(() => {
if (!getStoredToken()) {
router.replace("/login");
return;
}
// FIX: Implement role-based access control by checking the authenticated user's role from localStorage. Only users with "user" or "admin" roles can access the dashboard. If the role is missing or not allowed, clear the auth state and redirect to login.
const storedUser = getStoredUser(); // add this import at top of file
const storedUser = getStoredUser();
const role = typeof storedUser?.role === "string" ? storedUser.role : "";
if (!role || ["driver", "سائق"].includes(role)) {
clearAuth(); // add this import at top of file
clearAuth();
router.replace("/login");
return;
}
let mounted = true;
async function load() {
try {
const summary = await getDashboardSummary();
if (mounted) setData(summary);
} catch (err) {
if (mounted) setError(err instanceof Error ? err.message : "Unable to load dashboard data.");
} finally {
if (mounted) setLoading(false);
}
}
load();
return () => {
mounted = false;
};
}, [router]);
const stats = data?.data?.stats;
const alerts = data?.data?.alerts;
const activeTrips = data?.data?.activeTrips || [];
const stats = data?.stats;
const alerts = data?.alerts;
const activeTrips = data?.activeTrips || [];
const summaryCards = useMemo(
() => [
{ label: "العملاء", value: stats?.clients ?? 0, tone: "from-cyan-500 to-sky-600" },
{ label: "الطلبات", value: stats?.orders ?? 0, tone: "from-violet-500 to-fuchsia-600" },
{ label: "الرحلات", value: stats?.trips ?? 0, tone: "from-emerald-500 to-green-600" },
{ label: "المركبات", value: (stats?.cars ?? 0) + (stats?.drivers ?? 0), tone: "from-amber-400 to-orange-500" },
{ label: "العملاء", value: stats?.clients ?? 0, accent: "#06B6D4" },
{ label: "الطلبات", value: stats?.orders ?? 0, accent: "#A78BFA" },
{ label: "الرحلات", value: stats?.trips ?? 0, accent: "#34D399" },
{ label: "المركبات", value: (stats?.cars ?? 0) + (stats?.drivers ?? 0), accent: "#FBBF24" },
],
[stats],
);
return (
<main className="min-h-screen bg-[radial-gradient(circle_at_top,#0f172a_0%,#111827_45%,#020617_100%)] text-slate-100">
<section className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-6 py-10 lg:px-8">
<header className="rounded-3xl border border-white/10 bg-white/6 p-8 shadow-2xl shadow-slate-950/30 backdrop-blur-xl">
<p className="text-sm uppercase tracking-[0.35em] text-cyan-200">لوحة العمليات</p>
<div className="mt-4 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 className="text-3xl font-semibold text-white lg:text-4xl">ذكاء الأسطول في لمحة سريعة</h1>
<p className="mt-3 max-w-3xl text-slate-300">هذه الشاشة متوافقة مع نقطة ملخص لوحة الإدارة في الخلفية وتوفر عرضًا واضحًا لطلب الأسطول، وتنبيهات السلامة، وتقدم الرحلات.</p>
</div>
<Link href="/" className="inline-flex items-center rounded-full border border-cyan-400/30 bg-cyan-400/10 px-4 py-2 text-sm text-cyan-100 transition hover:bg-cyan-400/20">العودة إلى بوابة العميل</Link>
<section className="flex flex-col gap-6">
{/* ── Header ── */}
<header
style={{
background: "rgba(255,255,255,0.04)",
border: "1px solid var(--color-border-dark)",
borderRadius: "var(--radius-2xl)",
padding: "2rem",
boxShadow: "var(--shadow-overlay)",
backdropFilter: "blur(16px)",
}}
>
<p style={{ fontSize: 11, letterSpacing: "0.35em", textTransform: "uppercase", color: "#67E8F9" }}>
لوحة العمليات
</p>
<div className="mt-4 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 style={{ fontSize: "clamp(1.5rem,3vw,2rem)", fontWeight: 600, color: "var(--color-text-dark-primary)", margin: 0 }}>
ذكاء الأسطول في لمحة سريعة
</h1>
<p style={{ marginTop: "0.75rem", maxWidth: 680, color: "var(--color-text-dark-muted)", fontSize: 14, lineHeight: 1.6 }}>
عرض واضح لطلبات الأسطول وتنبيهات السلامة وتقدم الرحلات.
</p>
</div>
</header>
<Link
href="/"
style={{
display: "inline-flex",
alignItems: "center",
borderRadius: "var(--radius-full)",
border: "1px solid rgba(103,232,249,0.30)",
background: "rgba(103,232,249,0.08)",
padding: "0.5rem 1rem",
fontSize: 13,
color: "#CFFAFE",
textDecoration: "none",
whiteSpace: "nowrap",
transition: "var(--transition-base)",
}}
>
العودة إلى بوابة العميل
</Link>
</div>
</header>
{loading && <p className="text-slate-300">جارٍ تحميل مقاييس الخلفية</p>}
{error && <p className="rounded-2xl border border-rose-400/30 bg-rose-500/10 p-4 text-rose-100">{error}</p>}
{/* ── Loading ── */}
{loading && (
<div style={{ display: "flex", alignItems: "center", gap: 12, color: "var(--color-text-dark-muted)" }}>
<Spinner size="sm" className="text-cyan-400" />
<span style={{ fontSize: 14 }}>جارٍ تحميل البيانات</span>
</div>
)}
{!loading && !error && (
<>
<section className="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
{summaryCards.map((item) => (
<article key={item.label} className="rounded-3xl border border-white/10 bg-slate-900/80 p-5 shadow-xl shadow-slate-950/20">
<div className={`h-2 rounded-full bg-linear-to-r ${item.tone}`} />
<p className="mt-4 text-sm uppercase tracking-[0.25em] text-slate-400">{item.label}</p>
<p className="mt-2 text-4xl font-semibold text-white">{item.value}</p>
</article>
))}
</section>
{/* ── Error ── */}
{error && (
<Alert type="error" message={error}
className="border-rose-400/30 bg-rose-500/10 text-rose-100" />
)}
<section className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<article className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20">
<div className="flex items-center justify-between">
<div>
<p className="text-sm uppercase tracking-[0.25em] text-cyan-200">الرحلات النشطة</p>
<h2 className="mt-2 text-xl font-semibold text-white">تقدم الرحلات</h2>
</div>
<span className="rounded-full bg-emerald-400/10 px-3 py-1 text-xs text-emerald-200">مباشر من /dashboard/summary</span>
{!loading && !error && (
<>
{/* ── Summary cards ── */}
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{summaryCards.map((item) => (
<article
key={item.label}
style={{
borderRadius: "var(--radius-2xl)",
border: "1px solid var(--color-border-dark)",
background: "var(--color-surface-dark-card)",
padding: "1.25rem",
boxShadow: "var(--shadow-card)",
}}
>
<div style={{ height: 3, borderRadius: 999, background: item.accent, marginBottom: "1rem" }} />
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-dark-muted)" }}>
{item.label}
</p>
<p style={{ fontSize: "2.25rem", fontWeight: 600, color: "var(--color-text-dark-primary)", marginTop: "0.5rem" }}>
{item.value}
</p>
</article>
))}
</div>
{/* ── Active trips + alerts ── */}
<div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<article style={{ borderRadius: "var(--radius-2xl)", border: "1px solid var(--color-border-dark)", background: "var(--color-surface-dark-card)", padding: "1.5rem", boxShadow: "var(--shadow-card)" }}>
<div className="flex items-center justify-between">
<div>
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "#67E8F9" }}>الرحلات النشطة</p>
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, color: "var(--color-text-dark-primary)", marginTop: "0.5rem" }}>تقدم الرحلات</h2>
</div>
<div className="mt-6 space-y-4">
{activeTrips.length ? activeTrips.map((trip) => (
<div key={trip.id} className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-semibold text-white">{trip.tripNumber}</p>
<p className="text-sm text-slate-300">{trip.title}</p>
</div>
<span className="rounded-full bg-cyan-400/10 px-3 py-1 text-xs text-cyan-100">{trip.progress}%</span>
</div>
<div className="mt-3 h-2 rounded-full bg-slate-700">
<div className="h-2 rounded-full bg-linear-to-r from-cyan-400 to-emerald-400" style={{ width: `${trip.progress}%` }} />
<span style={{ borderRadius: "var(--radius-full)", background: "rgba(52,211,153,0.10)", padding: "0.25rem 0.75rem", fontSize: 11, color: "#A7F3D0" }}>
مباشر
</span>
</div>
<div style={{ marginTop: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
{activeTrips.length ? activeTrips.map((trip) => (
<div key={trip.id} style={{ borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border-dark)", background: "var(--color-surface-dark-raised)", padding: "1rem" }}>
<div className="flex items-center justify-between gap-3">
<div>
<p style={{ fontSize: 13, fontWeight: 600, color: "var(--color-text-dark-primary)" }}>{trip.tripNumber}</p>
<p style={{ fontSize: 13, color: "var(--color-text-dark-muted)" }}>{trip.title}</p>
</div>
<span style={{ borderRadius: "var(--radius-full)", background: "rgba(103,232,249,0.10)", padding: "0.25rem 0.75rem", fontSize: 11, color: "#CFFAFE" }}>
{trip.progress}%
</span>
</div>
)) : <p className="text-sm text-slate-300">لا توجد رحلات في-progress حالياً.</p>}
</div>
</article>
<div style={{ marginTop: "0.75rem", height: 6, borderRadius: 999, background: "rgba(255,255,255,0.08)" }}>
<div style={{ height: 6, borderRadius: 999, width: `${trip.progress}%`, background: "linear-gradient(90deg,#06B6D4,#34D399)" }} />
</div>
</div>
)) : (
<p style={{ fontSize: 13, color: "var(--color-text-dark-muted)" }}>لا توجد رحلات نشطة حالياً.</p>
)}
</div>
</article>
<article className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20">
<p className="text-sm uppercase tracking-[0.25em] text-amber-200">تنبيهات الالتزام</p>
<h2 className="mt-2 text-xl font-semibold text-white">التجديدات القادمة</h2>
<div className="mt-6 space-y-4 text-sm text-slate-200">
{(alerts?.expiringCars || []).slice(0, 3).map((item, index) => (
<div key={`car-${index}`} className="rounded-2xl border border-amber-400/20 bg-amber-400/8 p-4">{String((item as Record<string, unknown>).message || "Vehicle expiry alert")}</div>
))}
{(alerts?.expiringDrivers || []).slice(0, 3).map((item, index) => (
<div key={`driver-${index}`} className="rounded-2xl border border-rose-400/20 bg-rose-500/8 p-4">{String((item as Record<string, unknown>).message || "Driver expiry alert")}</div>
))}
{(alerts?.upcomingMaint || []).slice(0, 3).map((item, index) => (
<div key={`maint-${index}`} className="rounded-2xl border border-cyan-400/20 bg-cyan-400/8 p-4">{String((item as Record<string, unknown>).message || "Maintenance alert")}</div>
))}
</div>
</article>
</section>
<article style={{ borderRadius: "var(--radius-2xl)", border: "1px solid var(--color-border-dark)", background: "var(--color-surface-dark-card)", padding: "1.5rem", boxShadow: "var(--shadow-card)" }}>
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "#FCD34D" }}>تنبيهات الالتزام</p>
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, color: "var(--color-text-dark-primary)", marginTop: "0.5rem" }}>التجديدات القادمة</h2>
<div style={{ marginTop: "1.5rem", display: "flex", flexDirection: "column", gap: "0.75rem" }}>
{(alerts?.expiringCars || []).slice(0, 3).map((item, i) => (
<div key={`car-${i}`} style={{ borderRadius: "var(--radius-xl)", border: "1px solid rgba(251,191,36,0.20)", background: "rgba(251,191,36,0.06)", padding: "0.75rem 1rem", fontSize: 13, color: "var(--color-text-dark-primary)" }}>
{String((item as Record<string, unknown>).message || "Vehicle expiry alert")}
</div>
))}
{(alerts?.expiringDrivers || []).slice(0, 3).map((item, i) => (
<div key={`driver-${i}`} style={{ borderRadius: "var(--radius-xl)", border: "1px solid rgba(251,113,133,0.20)", background: "rgba(244,63,94,0.06)", padding: "0.75rem 1rem", fontSize: 13, color: "var(--color-text-dark-primary)" }}>
{String((item as Record<string, unknown>).message || "Driver expiry alert")}
</div>
))}
{(alerts?.upcomingMaint || []).slice(0, 3).map((item, i) => (
<div key={`maint-${i}`} style={{ borderRadius: "var(--radius-xl)", border: "1px solid rgba(103,232,249,0.20)", background: "rgba(103,232,249,0.06)", padding: "0.75rem 1rem", fontSize: 13, color: "var(--color-text-dark-primary)" }}>
{String((item as Record<string, unknown>).message || "Maintenance alert")}
</div>
))}
{!(alerts?.expiringCars?.length || alerts?.expiringDrivers?.length || alerts?.upcomingMaint?.length) && (
<p style={{ fontSize: 13, color: "var(--color-text-dark-muted)" }}>لا توجد تنبيهات حالياً.</p>
)}
</div>
</article>
</div>
<section className="grid gap-6 lg:grid-cols-[0.9fr_1.1fr]">
<article className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20">
<p className="text-sm uppercase tracking-[0.25em] text-violet-200">الأمان</p>
<h2 className="mt-2 text-xl font-semibold text-white">لمحة الحساب</h2>
<div className="mt-6 space-y-4">
<div className="rounded-2xl border border-white/10 bg-slate-800/80 p-4 text-sm text-slate-200">آخر تسجيل دخول: {data?.data?.accountSecurity?.lastLogin || "لم يتم العثور على حدث تسجيل دخول"}</div>
<div className="rounded-2xl border border-white/10 bg-slate-800/80 p-4 text-sm text-slate-200">بيانات الجهاز: {data?.data?.accountSecurity?.requestMeta ? JSON.stringify(data.data.accountSecurity.requestMeta) : "غير متاح"}</div>
{/* ── Security + endpoints ── */}
<div className="grid gap-6 lg:grid-cols-2">
<article style={{ borderRadius: "var(--radius-2xl)", border: "1px solid var(--color-border-dark)", background: "var(--color-surface-dark-card)", padding: "1.5rem", boxShadow: "var(--shadow-card)" }}>
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "#C4B5FD" }}>الأمان</p>
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, color: "var(--color-text-dark-primary)", marginTop: "0.5rem" }}>لمحة الحساب</h2>
<div style={{ marginTop: "1.5rem", display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<div style={{ borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border-dark)", background: "var(--color-surface-dark-raised)", padding: "0.875rem 1rem", fontSize: 13, color: "var(--color-text-dark-muted)" }}>
آخر تسجيل دخول: {data?.accountSecurity?.lastLogin || "—"}
</div>
</article>
<div style={{ borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border-dark)", background: "var(--color-surface-dark-raised)", padding: "0.875rem 1rem", fontSize: 13, color: "var(--color-text-dark-muted)" }}>
بيانات الجهاز: {data?.accountSecurity?.requestMeta ? JSON.stringify(data.accountSecurity.requestMeta) : "—"}
</div>
</div>
</article>
<article className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/20">
<p className="text-sm uppercase tracking-[0.25em] text-emerald-200">خريطة الخلفية</p>
<h2 className="mt-2 text-xl font-semibold text-white">النقاط النهائية المستندة إلى Swagger</h2>
<ul className="mt-6 grid gap-3 text-sm text-slate-200">
<li className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">/v1/dashboard/summary نظرة عامة تشغيلية</li>
<li className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">/v1/client إدارة العملاء</li>
<li className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">/v1/orders دورة شحن الطلبات</li>
<li className="rounded-2xl border border-white/10 bg-slate-800/80 p-4">/v1/trip تنظيم الرحلات</li>
</ul>
</article>
</section>
</>
)}
</section>
</main>
<article style={{ borderRadius: "var(--radius-2xl)", border: "1px solid var(--color-border-dark)", background: "var(--color-surface-dark-card)", padding: "1.5rem", boxShadow: "var(--shadow-card)" }}>
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "#6EE7B7" }}>خريطة الخلفية</p>
<h2 style={{ fontSize: "1.25rem", fontWeight: 600, color: "var(--color-text-dark-primary)", marginTop: "0.5rem" }}>النقاط النهائية</h2>
<ul style={{ marginTop: "1.5rem", display: "flex", flexDirection: "column", gap: "0.75rem", listStyle: "none", padding: 0 }}>
{[
"/v1/dashboard/summary — نظرة عامة تشغيلية",
"/v1/client — إدارة العملاء",
"/v1/orders — دورة شحن الطلبات",
"/v1/trip — تنظيم الرحلات",
].map((ep) => (
<li key={ep} style={{ borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border-dark)", background: "var(--color-surface-dark-raised)", padding: "0.875rem 1rem", fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--color-text-dark-muted)" }}>
{ep}
</li>
))}
</ul>
</article>
</div>
</>
)}
</section>
);
}
}

View File

@@ -1,3 +1,247 @@
export default function DriversPage() {
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Drivers module is ready for the workforce and report tools.</section>;
"use client";
import { useEffect, useState } from "react";
import { getStoredToken } from "../../lib/auth";
import { get, post, put, del } from "../../services/api";
import { Spinner, Alert } from "../../Components/UI";
interface Driver {
id: string;
name: string;
userName?: string;
phone: string;
nationality?: string;
licenseNumber?: string;
licenseExpiry?: string;
nationalIdExpiry?: string;
status: "Active" | "Inactive" | "InTrip" | "Suspended";
isActive: boolean;
branch?: { name: string };
createdAt: string;
}
interface ApiResponse {
data: {
data: Driver[];
pagination: { total: number; page: number; pages: number };
meta?: { total: number; pages: number };
};
}
const STATUS_MAP: Record<Driver["status"], { label: string; color: string; bg: string; border: string }> = {
Active: { label: "نشط", color: "#166534", bg: "#DCFCE7", border: "#BBF7D0" },
InTrip: { label: "في رحلة", color: "#1E40AF", bg: "#EFF6FF", border: "#BFDBFE" },
Inactive: { label: "غير نشط", color: "#64748B", bg: "#F1F5F9", border: "#E2E8F0" },
Suspended: { label: "موقوف", color: "#991B1B", bg: "#FEF2F2", border: "#FECACA" },
};
function fmtDate(iso?: string) {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("ar-SA", {
year: "numeric", month: "short", day: "numeric",
});
}
function expirySoon(iso?: string) {
if (!iso) return false;
return (new Date(iso).getTime() - Date.now()) / 86_400_000 <= 90;
}
const cardStyle: React.CSSProperties = {
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
overflow: "hidden",
boxShadow: "var(--shadow-card)",
};
const thStyle: React.CSSProperties = {
padding: "0.75rem 1.5rem",
fontSize: 11,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.2em",
color: "var(--color-text-muted)",
background: "var(--color-surface-muted)",
borderBottom: "1px solid var(--color-border)",
};
export default function DriversPage() {
const [drivers, setDrivers] = useState<Driver[]>([]);
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);
useEffect(() => {
const token = getStoredToken();
setLoading(true);
setError(null);
const query = `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
get<ApiResponse>(`driver${query}`, token)
.then((res) => {
const payload = res.data ?? res;
setDrivers(payload.data ?? []);
setTotal(payload.meta?.total ?? 0);
setPages(payload.meta?.pages ?? 1);
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [page, search]);
return (
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* 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)",
}}>
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600 }}>
إدارة الكوادر
</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 }}>
السائقون
</h1>
<p style={{ marginTop: "0.25rem", fontSize: 13, color: "var(--color-text-muted)" }}>
إجمالي <strong style={{ color: "var(--color-text-primary)" }}>{total}</strong> سائق مسجل
</p>
</div>
<div style={{ position: "relative", width: "100%", maxWidth: 288 }}>
<svg style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", width: 16, height: 16, color: "var(--color-text-hint)", pointerEvents: "none" }}
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) => { setSearch(e.target.value); setPage(1); }} dir="rtl"
style={{
width: "100%", height: 40, paddingRight: 36, paddingLeft: 12,
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13, color: "var(--color-text-primary)",
outline: "none", fontFamily: "var(--font-sans)",
}}
/>
</div>
</div>
</header>
{error && <Alert type="error" message={error} onClose={() => setError(null)} />}
{/* Table */}
<div style={cardStyle}>
<div dir="rtl" style={{
display: "grid",
gridTemplateColumns: "2fr 1.2fr 1fr 1fr 1fr 1fr",
...thStyle,
}}>
<span>السائق</span>
<span>الفرع</span>
<span>الجنسية</span>
<span>انتهاء الرخصة</span>
<span>الحالة</span>
<span style={{ textAlign: "center" }}>انتهاء الهوية</span>
</div>
{loading ? (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "4rem 0", color: "var(--color-text-muted)" }}>
<Spinner size="sm" className="text-blue-600" />
<span style={{ fontSize: 13 }}>جارٍ التحميل</span>
</div>
) : drivers.length === 0 ? (
<p style={{ textAlign: "center", padding: "4rem 0", fontSize: 13, color: "var(--color-text-muted)" }}>
لا توجد نتائج {search && `لـ "${search}"`}
</p>
) : (
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
{drivers.map((d, i) => {
const status = STATUS_MAP[d.status];
const licWarn = expirySoon(d.licenseExpiry);
const idWarn = expirySoon(d.nationalIdExpiry);
return (
<li key={d.id} style={{
display: "grid",
gridTemplateColumns: "2fr 1.2fr 1fr 1fr 1fr 1fr",
alignItems: "center", gap: "0.5rem",
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent",
fontSize: 13,
}}>
<div>
<p style={{ fontWeight: 600, color: "var(--color-text-primary)", margin: 0 }}>{d.name}</p>
<p style={{ marginTop: 2, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-muted)" }}>{d.phone}</p>
</div>
<span style={{ color: "var(--color-text-secondary)" }}>{d.branch?.name ?? "—"}</span>
<span style={{ color: "var(--color-text-secondary)" }}>{d.nationality ?? "—"}</span>
<span style={{
fontSize: 12, fontWeight: 600,
color: licWarn ? "#D97706" : "var(--color-text-secondary)",
}}>
{licWarn && "⚠ "}{fmtDate(d.licenseExpiry)}
</span>
<span style={{
display: "inline-flex", alignItems: "center",
borderRadius: "var(--radius-full)",
border: `1px solid ${status.border}`,
background: status.bg,
padding: "0.2rem 0.625rem",
fontSize: 11, fontWeight: 600, color: status.color,
width: "fit-content",
}}>
{status.label}
</span>
<span style={{
textAlign: "center", fontSize: 12, fontWeight: 600,
color: idWarn ? "#D97706" : "var(--color-text-secondary)",
}}>
{idWarn && "⚠ "}{fmtDate(d.nationalIdExpiry)}
</span>
</li>
);
})}
</ul>
)}
{pages > 1 && (
<div dir="rtl" style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
borderTop: "1px solid var(--color-border)", padding: "0.875rem 1.5rem",
}}>
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
صفحة <strong style={{ color: "var(--color-text-primary)" }}>{page}</strong> من <strong style={{ color: "var(--color-text-primary)" }}>{pages}</strong>
</span>
<div style={{ display: "flex", gap: "0.5rem" }}>
{[
{ label: "السابق", action: () => setPage(p => Math.max(1, p - 1)), disabled: page === 1 },
{ label: "التالي", action: () => setPage(p => Math.min(pages, p + 1)), disabled: page === pages },
].map(btn => (
<button key={btn.label} onClick={btn.action} disabled={btn.disabled}
style={{
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
padding: "0.375rem 0.875rem",
fontSize: 12, color: "var(--color-text-secondary)",
cursor: btn.disabled ? "not-allowed" : "pointer",
opacity: btn.disabled ? 0.4 : 1,
fontFamily: "var(--font-sans)",
}}>
{btn.label}
</button>
))}
</div>
</div>
)}
</div>
</section>
);
}

View File

@@ -1,6 +1,7 @@
@import "../styles/tokens.css";
@import "tailwindcss";
/* ── Base resets ────────────────────────────────────── */
*,
*::before,
@@ -27,11 +28,60 @@ body {
/* ── Tailwind theme bridge ──────────────────────────── */
/* Expose CSS variables as Tailwind color utilities. */
@theme inline {
--color-background: var(--color-surface-sunken);
--color-foreground: var(--color-text-primary);
--color-brand: var(--color-brand-600);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--color-primary-*: initial;
--color-sand-*: initial;
--color-gold-*: initial;
--color-slate-*: initial;
--color-primary-50: var(--color-primary-50);
--color-primary-100: var(--color-primary-100);
--color-primary-200: var(--color-primary-200);
--color-primary-300: var(--color-primary-300);
--color-primary-400: var(--color-primary-400);
--color-primary-500: var(--color-primary-500);
--color-primary-600: var(--color-primary-600);
--color-primary-700: var(--color-primary-700);
--color-primary-800: var(--color-primary-800);
--color-primary-900: var(--color-primary-900);
--color-sand-50: var(--color-sand-50);
--color-sand-100: var(--color-sand-100);
--color-sand-200: var(--color-sand-200);
--color-sand-300: var(--color-sand-300);
--color-sand-400: var(--color-sand-400);
--color-sand-500: var(--color-sand-500);
--color-sand-600: var(--color-sand-600);
--color-sand-700: var(--color-sand-700);
--color-sand-800: var(--color-sand-800);
--color-sand-900: var(--color-sand-900);
--color-gold-50: var(--color-gold-50);
--color-gold-100: var(--color-gold-100);
--color-gold-200: var(--color-gold-200);
--color-gold-300: var(--color-gold-300);
--color-gold-400: var(--color-gold-400);
--color-gold-500: var(--color-gold-500);
--color-gold-600: var(--color-gold-600);
--color-gold-700: var(--color-gold-700);
--color-gold-800: var(--color-gold-800);
--color-gold-900: var(--color-gold-900);
--color-slate-50: var(--color-slate-50);
--color-slate-100: var(--color-slate-100);
--color-slate-200: var(--color-slate-200);
--color-slate-300: var(--color-slate-300);
--color-slate-400: var(--color-slate-400);
--color-slate-500: var(--color-slate-500);
--color-slate-600: var(--color-slate-600);
--color-slate-700: var(--color-slate-700);
--color-slate-800: var(--color-slate-800);
--color-slate-900: var(--color-slate-900);
--color-background: var(--color-sand-50);
--color-foreground: var(--color-slate-900);
--color-brand: var(--color-primary-500);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
}
/* ── Focus ring utility ─────────────────────────────── */

View File

@@ -4,38 +4,32 @@ import Link from "next/link";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { loginUser } from "../../lib/auth";
import Logo from "@/utils/logo";
import { Spinner } from "@/Components/UI";
import Logo from "../../utils/logo";
import { Button } from "../../Components/UI";
import { Input } from "../../Components/UI";
import { Alert } from "../../Components/UI";
export default function LoginPage() {
const router = useRouter();
const [identity, setIdentity] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
setLoading(true);
try {
await loginUser(identity, password);
window.location.href = "/dashboard"; // Use full page reload to ensure all client state is reset after login
console.log("Login successful", { identity });
window.location.href = "/dashboard";
} catch (err) {
// FIX: Surface specific error messages from auth.ts (invalid credentials,
// role denial, network failure) directly to the user.
if (err instanceof Error) {
// Network/fetch failure signals
if (
err.message.includes("fetch") ||
err.message.includes("network") ||
err.message.includes("Failed to fetch")
) {
setError(
"Unable to connect. Please check your internet connection and try again later.",
);
const msg = err.message.toLowerCase();
if (msg.includes("fetch") || msg.includes("network") || msg.includes("failed to fetch")) {
setError("Unable to connect. Please check your internet connection and try again later.");
} else {
setError(err.message);
}
@@ -50,45 +44,37 @@ export default function LoginPage() {
return (
<main className="min-h-screen bg-slate-100 text-slate-900">
<section className="grid min-h-screen w-full lg:grid-cols-[1fr_1fr]">
{/* ── Left panel ─────────────────────────────────── */}
<aside className="relative overflow-hidden bg-linear-to-br from-blue-900 via-blue-600 to-blue-700 px-8 py-10 text-white lg:px-12 lg:py-14">
<div
aria-hidden="true"
className="absolute inset-0 opacity-30"
style={{
backgroundImage:
"radial-gradient(circle, rgba(255,255,255,0.18) 1px, transparent 1px)",
backgroundImage: "radial-gradient(circle, rgba(255,255,255,0.18) 1px, transparent 1px)",
backgroundSize: "40px 40px",
}}
/>
<div className="relative z-10 flex h-full flex-col justify-between">
<div>
<Logo />
<Logo white={true} />
<h1 className="mt-10 max-w-md text-[36px] font-bold leading-tight tracking-[-0.5px]">
اللوجستيات ببساطة، والتسليم بثقة.
</h1>
<p className="mt-4 max-w-md text-[15px] leading-7 text-white/75">
راقب عمليات التسليم، وقم بتنظيم السائقين، وحافظ على رؤية كل طلب
من خلال تسجيل دخول آمن واحد.
راقب عمليات التسليم، وقم بتنظيم السائقين، وحافظ على رؤية كل طلب من خلال تسجيل دخول آمن واحد.
</p>
<div className="mt-8 space-y-3">
{[
[
"التنسيق اللحظي",
"تتبع كل طلب من الاستلام إلى التسليم في مكان واحد.",
],
[
"وصول آمن",
"الجلسات المعتمدة على الصلاحيات تبقي مسارات العميل والإدارة منفصلة.",
],
[
"عمليات سريعة",
"استخدم لوحة الإدارة لمراجعة الحالة والتنبيهات وحركة الرحلات.",
],
["التنسيق اللحظي", "تتبع كل طلب من الاستلام إلى التسليم في مكان واحد."],
["وصول آمن", "الجلسات المعتمدة على الصلاحيات تبقي مسارات العميل والإدارة منفصلة."],
["عمليات سريعة", "استخدم لوحة الإدارة لمراجعة الحالة والتنبيهات وحركة الرحلات."],
].map(([title, desc]) => (
<article
key={title}
className="flex items-start gap-3 rounded-[10px] border border-white/15 bg-white/10 p-3"
>
<div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-white/15 text-sm">
<div aria-hidden="true" className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-white/15 text-sm">
</div>
<div>
@@ -99,71 +85,61 @@ export default function LoginPage() {
))}
</div>
</div>
<p className="text-[12px] text-white/50">
مصمم لفرق العمليات التي تحتاج إلى الوضوح والسرعة والثقة.
</p>
<p className="text-[12px] text-white/50">مصمم لفرق العمليات التي تحتاج إلى الوضوح والسرعة والثقة.</p>
</div>
</aside>
{/* ── Right panel ────────────────────────────────── */}
<section className="flex items-center justify-center bg-slate-50 px-6 py-10 lg:px-8">
<form
onSubmit={handleSubmit}
className="w-full max-w-100 rounded-[14px] border border-slate-200 bg-white p-8 shadow-sm"
className="w-full max-w-[400px] rounded-[14px] border border-slate-200 bg-white p-8 shadow-sm"
noValidate
>
<p className="text-[28px] font-bold text-slate-900">
مرحبًا بعودتك
</p>
<p className="mt-1 text-[14px] text-slate-500">
سجل الدخول لإدارة عمليات التسليم
</p>
<p className="text-[28px] font-bold text-slate-900">مرحبًا بعودتك</p>
<p className="mt-1 text-[14px] text-slate-500">سجل الدخول لإدارة عمليات التسليم</p>
<label className="mt-6 block text-[11px] font-bold uppercase tracking-[0.5px] text-slate-500">
البريد الإلكتروني أو رقم الهاتف أو اسم المستخدم
<input
<div className="mt-6 flex flex-col gap-4">
<Input
label="البريد الإلكتروني أو رقم الهاتف أو اسم المستخدم"
autoComplete="username"
className="mt-2 h-11 w-full rounded-[9px] border border-slate-200 bg-white px-3 text-[14px] text-slate-900 outline-none transition focus:border-blue-500 focus:ring-4 focus:ring-blue-100"
value={identity}
onChange={(event) => setIdentity(event.target.value)}
onChange={(e) => setIdentity(e.target.value)}
placeholder="name@company.com / 05xxxxxxxx / username"
required
/>
</label>
<label className="mt-4 block text-[11px] font-bold uppercase tracking-[0.5px] text-slate-500">
كلمة المرور
<input
<Input
label="كلمة المرور"
type="password"
autoComplete="current-password"
className="mt-2 h-11 w-full rounded-[9px] border border-slate-200 bg-white px-3 text-[14px] text-slate-900 outline-none transition focus:border-blue-500 focus:ring-4 focus:ring-blue-100"
value={password}
onChange={(event) => setPassword(event.target.value)}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
</label>
</div>
<div className="mt-3 flex justify-end">
<Link
href="/forgot-password"
className="text-[12px] text-blue-600 hover:underline"
>
<Link href="/forgot-password" className="text-[12px] text-blue-600 hover:underline">
هل نسيت كلمة المرور؟
</Link>
</div>
{error ? (
<p className="mt-4 rounded-[9px] border border-red-200 bg-red-50 p-3 text-[13px] text-red-600">
{error}
</p>
) : null}
{error && (
<div className="mt-4">
<Alert type="error" message={error} onClose={() => setError(null)} />
</div>
)}
<button
<Button
type="submit"
disabled={loading}
className="mt-6 inline-flex h-11.5 w-full items-center justify-center rounded-[9px] bg-blue-600 text-[15px] font-semibold text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-70"
loading={loading}
fullWidth
className="mt-6 h-11"
>
{loading ? <Spinner /> : "تسجيل الدخول"}
</button>
{loading ? "جاري تسجيل الدخول…" : "تسجيل الدخول"}
</Button>
<p className="mt-4 text-center text-[13px] text-slate-500">
ليس لديك حساب؟{" "}
@@ -176,4 +152,4 @@ export default function LoginPage() {
</section>
</main>
);
}
}

View File

@@ -1,3 +1,281 @@
export default function OrdersPage() {
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Orders module is ready for the order lifecycle and transfer workflows.</section>;
"use client";
import { useEffect, useState } from "react";
import { getStoredToken } from "../../lib/auth";
import { get } from "../../services/api";
import { Spinner, Alert } from "../../Components/UI";
interface Order {
id: string;
shipmentNumber: string;
recipientName: string;
recipientPhone: string;
currentStatus: "Created" | "Assigned" | "InTransit" | "Delivered" | "Returned" | "Cancelled";
totalPrice?: number;
paymentMethod?: string;
paymentStatus?: "Pending" | "Paid" | "Failed" | "Refunded";
client?: { name: string };
trip?: { tripNumber: string };
createdAt: string;
}
interface ApiResponse {
data: {
data: Order[];
pagination: { total: number; page: number; pages: number };
meta?: { total: number; pages: number };
};
}
const STATUS_MAP: Record<Order["currentStatus"], { label: string; color: string; bg: string; border: string }> = {
Created: { label: "تم الإنشاء", color: "#1E40AF", bg: "#EFF6FF", border: "#BFDBFE" },
Assigned: { label: "مُعيَّن", color: "#5B21B6", bg: "#F5F3FF", border: "#DDD6FE" },
InTransit: { label: "قيد التوصيل", color: "#854D0E", bg: "#FFFBEB", border: "#FDE68A" },
Delivered: { label: "تم التسليم", color: "#166534", bg: "#DCFCE7", border: "#BBF7D0" },
Returned: { label: "مُرتجع", color: "#991B1B", bg: "#FEF2F2", border: "#FECACA" },
Cancelled: { label: "ملغي", color: "#64748B", bg: "#F1F5F9", border: "#E2E8F0" },
};
const PAY_MAP: Record<string, { label: string; color: string }> = {
Pending: { label: "معلَّق", color: "#D97706" },
Paid: { label: "مدفوع", color: "#16A34A" },
Failed: { label: "فشل", color: "#DC2626" },
Refunded: { label: "مُسترجع", color: "#64748B" },
};
const cardStyle: React.CSSProperties = {
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
overflow: "hidden",
boxShadow: "var(--shadow-card)",
};
const thStyle: React.CSSProperties = {
padding: "0.75rem 1.5rem",
fontSize: 11,
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.2em",
color: "var(--color-text-muted)",
background: "var(--color-surface-muted)",
borderBottom: "1px solid var(--color-border)",
};
export default function OrdersPage() {
const [orders, setOrders] = useState<Order[]>([]);
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);
const [statusFilter, setStatusFilter] = useState<string>("");
useEffect(() => {
const token = getStoredToken();
setLoading(true);
setError(null);
const params = new URLSearchParams({ page: String(page), limit: "10" });
if (search) params.set("search", search);
if (statusFilter) params.set("currentStatus", statusFilter);
get<ApiResponse>(`orders?${params}`, token)
.then((res) => {
const payload = res.data ?? res;
setOrders(payload.data ?? []);
setTotal(payload.meta?.total ?? 0);
setPages(payload.meta?.pages ?? 1);
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [page, search, statusFilter]);
return (
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* 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)",
}}>
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600 }}>
إدارة الشحنات
</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 }}>
الطلبات
</h1>
<p style={{ marginTop: "0.25rem", fontSize: 13, color: "var(--color-text-muted)" }}>
إجمالي <strong style={{ color: "var(--color-text-primary)" }}>{total}</strong> طلب
</p>
</div>
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
{/* Status filter */}
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
dir="rtl"
style={{
height: 40, borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13, color: "var(--color-text-secondary)",
padding: "0 0.75rem", outline: "none",
fontFamily: "var(--font-sans)",
}}
>
<option value="">كل الحالات</option>
{Object.entries(STATUS_MAP).map(([k, v]) => (
<option key={k} value={k}>{v.label}</option>
))}
</select>
{/* Search */}
<div style={{ position: "relative", width: 256 }}>
<svg style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", width: 16, height: 16, color: "var(--color-text-hint)", pointerEvents: "none" }}
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) => { setSearch(e.target.value); setPage(1); }} dir="rtl"
style={{
width: "100%", height: 40, paddingRight: 36, paddingLeft: 12,
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13, color: "var(--color-text-primary)",
outline: "none", fontFamily: "var(--font-sans)",
}}
/>
</div>
</div>
</div>
</header>
{error && <Alert type="error" message={error} onClose={() => setError(null)} />}
{/* Table */}
<div style={cardStyle}>
<div dir="rtl" style={{
display: "grid",
gridTemplateColumns: "1.8fr 1.5fr 1.2fr 1fr 1fr 1fr",
...thStyle,
}}>
<span>رقم الشحنة</span>
<span>المستلم</span>
<span>العميل</span>
<span>المبلغ</span>
<span>الحالة</span>
<span style={{ textAlign: "center" }}>التاريخ</span>
</div>
{loading ? (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "4rem 0", color: "var(--color-text-muted)" }}>
<Spinner size="sm" className="text-blue-600" />
<span style={{ fontSize: 13 }}>جارٍ التحميل</span>
</div>
) : orders.length === 0 ? (
<p style={{ textAlign: "center", padding: "4rem 0", fontSize: 13, color: "var(--color-text-muted)" }}>
لا توجد نتائج
</p>
) : (
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
{orders.map((o, i) => {
const status = STATUS_MAP[o.currentStatus];
const pay = o.paymentStatus ? PAY_MAP[o.paymentStatus] : null;
return (
<li key={o.id} style={{
display: "grid",
gridTemplateColumns: "1.8fr 1.5fr 1.2fr 1fr 1fr 1fr",
alignItems: "center", gap: "0.5rem",
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent",
fontSize: 13,
}}>
<div>
<p style={{ fontFamily: "var(--font-mono)", fontSize: 12, fontWeight: 700, color: "#2563EB", margin: 0 }}>
{o.shipmentNumber}
</p>
{o.trip && (
<p style={{ marginTop: 2, fontSize: 11, color: "var(--color-text-muted)" }}>
رحلة: {o.trip.tripNumber}
</p>
)}
</div>
<div>
<p style={{ color: "var(--color-text-primary)", margin: 0 }}>{o.recipientName}</p>
<p style={{ marginTop: 2, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-muted)" }}>
{o.recipientPhone}
</p>
</div>
<span style={{ color: "var(--color-text-secondary)" }}>{o.client?.name ?? "—"}</span>
<div>
<p style={{ fontWeight: 600, color: "var(--color-text-primary)", margin: 0 }}>
{o.totalPrice != null ? `${o.totalPrice.toFixed(2)} ر.س` : "—"}
</p>
{pay && (
<p style={{ marginTop: 2, fontSize: 11, fontWeight: 600, color: pay.color }}>
{pay.label}
</p>
)}
</div>
<span style={{
display: "inline-flex", alignItems: "center",
borderRadius: "var(--radius-full)",
border: `1px solid ${status.border}`,
background: status.bg,
padding: "0.2rem 0.625rem",
fontSize: 11, fontWeight: 600, color: status.color,
width: "fit-content",
}}>
{status.label}
</span>
<span style={{ textAlign: "center", fontSize: 11, color: "var(--color-text-muted)" }}>
{new Date(o.createdAt).toLocaleDateString("ar-SA", {
year: "numeric", month: "short", day: "numeric",
})}
</span>
</li>
);
})}
</ul>
)}
{pages > 1 && (
<div dir="rtl" style={{
display: "flex", alignItems: "center", justifyContent: "space-between",
borderTop: "1px solid var(--color-border)", padding: "0.875rem 1.5rem",
}}>
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
صفحة <strong style={{ color: "var(--color-text-primary)" }}>{page}</strong> من <strong style={{ color: "var(--color-text-primary)" }}>{pages}</strong>
</span>
<div style={{ display: "flex", gap: "0.5rem" }}>
{[
{ label: "السابق", action: () => setPage(p => Math.max(1, p - 1)), disabled: page === 1 },
{ label: "التالي", action: () => setPage(p => Math.min(pages, p + 1)), disabled: page === pages },
].map(btn => (
<button key={btn.label} onClick={btn.action} disabled={btn.disabled}
style={{
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
padding: "0.375rem 0.875rem",
fontSize: 12, color: "var(--color-text-secondary)",
cursor: btn.disabled ? "not-allowed" : "pointer",
opacity: btn.disabled ? 0.4 : 1,
fontFamily: "var(--font-sans)",
}}>
{btn.label}
</button>
))}
</div>
</div>
)}
</div>
</section>
);
}

View File

@@ -1,3 +1,137 @@
// صفحة إدارة المستخدمين — تستورد فقط من الطبقات الأخرى
"use client";
import { useState } from "react";
import { Alert } from "../../Components/UI";
import { UserTable } from "../../Components/User/UserTable";
import { UserFormModal } from "../../Components/User/UserFormModal";
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() {
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Users module is ready for the full CRUD UI.</section>;
}
// ── الحالة المحلية للمودالات فقط ─────────────────────────────────────────
// false = مغلق، null = وضع الإضافة، User = وضع التعديل
const [formTarget, setFormTarget] = useState<User | null | false>(false);
const [deleteTarget, setDeleteTarget] = useState<User | null>(null);
const [deleting, setDeleting] = useState(false);
// ── كل منطق البيانات والحالة من الهوك ────────────────────────────────────
const {
users, loading, total, pages, error,
roles, branches,
page, search,
setPage, handleSearch, clearError,
createUser, updateUser, deleteUser,
notification,
} = useUsers();
// ── معالج تقديم النموذج (إضافة أو تعديل) ────────────────────────────────
const handleFormSubmit = async (data: UserFormData, isNew: boolean): Promise<boolean> => {
if (isNew) return createUser(data);
return updateUser((formTarget as User).id, data);
};
// ── معالج تأكيد الحذف ────────────────────────────────────────────────────
const handleDeleteConfirm = async () => {
if (!deleteTarget) return;
setDeleting(true);
const ok = await deleteUser(deleteTarget.id);
setDeleting(false);
if (ok) setDeleteTarget(null);
};
return (
<>
{/* ── Toast لعرض رسائل النجاح والخطأ ── */}
<Toast notification={notification} />
{/* ── مودال الإضافة / التعديل ── */}
{formTarget !== false && (
<UserFormModal
editUser={formTarget}
roles={roles}
branches={branches}
onClose={() => setFormTarget(false)}
onSubmit={handleFormSubmit}
/>
)}
{/* ── مودال تأكيد الحذف ── */}
{deleteTarget && (
<DeleteConfirmModal
user={deleteTarget}
deleting={deleting}
onCancel={() => setDeleteTarget(null)}
onConfirm={handleDeleteConfirm}
/>
)}
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* ── رأس الصفحة ── */}
<header style={{
borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)",
background: "var(--color-surface)", padding: "1.5rem 2rem", boxShadow: "var(--shadow-card)",
}}>
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600 }}>
إدارة الفريق
</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 }}>
المستخدمون
</h1>
<p style={{ marginTop: "0.25rem", fontSize: 13, color: "var(--color-text-muted)" }}>
إجمالي <strong style={{ color: "var(--color-text-primary)" }}>{total}</strong> مستخدم
</p>
</div>
{/* أدوات البحث وإضافة مستخدم */}
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}>
{/* حقل البحث */}
<div style={{ position: "relative", width: 256 }}>
<svg style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", width: 16, height: 16, color: "var(--color-text-hint)", pointerEvents: "none" }}
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)} dir="rtl"
style={{ width: "100%", height: 40, paddingRight: 36, paddingLeft: 12, borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, outline: "none", fontFamily: "var(--font-sans)" }}
/>
</div>
{/* زر إضافة مستخدم */}
<button type="button" onClick={() => setFormTarget(null)}
style={{ height: 40, padding: "0 1.125rem", borderRadius: "var(--radius-lg)", border: "none", background: "var(--color-brand-600)", fontSize: 13, fontWeight: 700, color: "#FFF", cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 7, fontFamily: "var(--font-sans)", boxShadow: "0 1px 4px rgba(37,99,235,.35)", whiteSpace: "nowrap" }}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
</svg>
إضافة مستخدم
</button>
</div>
</div>
</header>
{/* ── رسالة الخطأ العامة (فشل التحميل) ── */}
{error && (
<Alert type="error" message={error} onClose={clearError} />
)}
{/* ── جدول المستخدمين ── */}
<UserTable
users={users}
loading={loading}
search={search}
page={page}
pages={pages}
onEdit={user => setFormTarget(user)}
onDelete={user => setDeleteTarget(user)}
onAddFirst={() => setFormTarget(null)}
onPageChange={setPage}
/>
</section>
</>
);
}