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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user