handel login page and logout alson andel create order page to client and build spinner page and complet project structuer , create logo tamplet
This commit is contained in:
94
app/NewOrder/page.tsx
Normal file
94
app/NewOrder/page.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
// ─────────────────────────────────────────────
|
||||
// page.tsx
|
||||
// نقطة الدخول — تحقق من الجلسة وتوجيه المستخدم
|
||||
// ─────────────────────────────────────────────
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { loadSession, type ClientSession } from "@/src/utils/helperFun";
|
||||
import RegisterStep from "../../src/Components/Client/client";
|
||||
import DashboardStep from "../../src/Components/Order/order";
|
||||
import Logo from "@/src/utils/logo";
|
||||
|
||||
type Step = "bootstrap" | "register" | "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">
|
||||
<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">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
/* شاشة التحميل */
|
||||
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("dashboard");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen bg-[#ECEEF2]"
|
||||
dir="rtl"
|
||||
lang="ar"
|
||||
style={{ fontFamily: "'Cairo', 'IBM Plex Sans Arabic', Tahoma, sans-serif" }}
|
||||
>
|
||||
<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 === "register" && (
|
||||
<RegisterStep onSuccess={handleRegistered} />
|
||||
)}
|
||||
|
||||
{step === "dashboard" && session && (
|
||||
<DashboardStep session={session} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-[11px] text-[#9CA3AF] mt-4">
|
||||
LogiFlow · آمن ومشفر · المملكة العربية السعودية 🇸🇦
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function ClientsPage() {
|
||||
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Clients module is ready for the address and order management views.</section>;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import Logo from "@/src/utils/logo";
|
||||
import Link from "next/link";
|
||||
|
||||
|
||||
|
||||
@@ -11,26 +13,10 @@ const navLinks = [
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<nav dir="rtl" className="w-full border-b border-slate-200 bg-white text-slate-900 shadow-sm">
|
||||
<nav dir="rtl" className="w-full border-b border-slate-200 bg-white text-slate-900 shadow-sm">x
|
||||
<div className="mx-auto flex h-14 max-w-7xl items-center gap-4 px-6 lg:px-8">
|
||||
<a href="#" className="flex items-center gap-3 text-right">
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600 shadow-sm">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-5 w-5 text-white"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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>
|
||||
</span>
|
||||
<span className="text-[16px] font-extrabold text-slate-900">LogiFlow</span>
|
||||
<a href="/" className="flex items-center gap-3 text-right">
|
||||
<Logo />
|
||||
</a>
|
||||
|
||||
<div className="hidden flex-1 items-center justify-center gap-6 md:flex">
|
||||
@@ -48,12 +34,12 @@ export default function Navbar() {
|
||||
</div>
|
||||
|
||||
<div className="mr-auto flex items-center gap-2">
|
||||
<button 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">
|
||||
إنشاء حساب عميل
|
||||
</button>
|
||||
<button 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 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">
|
||||
تسجيل الدخول ←
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
// 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 { getStoredUser } from "../../../src/lib/auth";
|
||||
|
||||
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" },
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const permissions = navItems.map((item) => item.permission);
|
||||
// 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);
|
||||
|
||||
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">
|
||||
<aside
|
||||
suppressHydrationWarning
|
||||
className="hidden w-72 shrink-0 border-r border-white/10 bg-slate-950/80 p-6 lg:flex lg:flex-col"
|
||||
>
|
||||
<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>
|
||||
<p className="mt-2 text-sm text-slate-300">
|
||||
Fleet, clients, orders, and compliance in one place.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<nav className="mt-8 space-y-2">
|
||||
{navItems.map((item) => {
|
||||
const hasPermission = permissions.includes(item.permission) || item.permission === "read-dashboard";
|
||||
// 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;
|
||||
|
||||
return (
|
||||
@@ -46,9 +62,15 @@ export function Sidebar() {
|
||||
|
||||
<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>
|
||||
<p suppressHydrationWarning className="mt-2 font-semibold">Operations user</p>
|
||||
<p suppressHydrationWarning className="text-xs text-emerald-100/90">Signed in</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?.name ?? user?.userName ?? "—"}
|
||||
</p>
|
||||
<p suppressHydrationWarning className="text-xs text-emerald-100/90">
|
||||
{user?.role ? `Role: ${user.role}` : "Signed in"}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,60 @@
|
||||
// File: app/components/layout/Topbar.tsx
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { clearAuth } from "../../lib/auth";
|
||||
// FIX: Import both clearAuth and getStoredUser so the topbar can show the
|
||||
// authenticated user's role and handle logout properly.
|
||||
import { clearAuth, getStoredUser } from "../../../src/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">
|
||||
<header
|
||||
suppressHydrationWarning
|
||||
className="border-b border-white/10 bg-slate-950/70 px-4 py-4 lg:px-6"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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">Manager</div>
|
||||
<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}
|
||||
@@ -34,4 +66,4 @@ export function Topbar() {
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { getDashboardSummary, type DashboardSummaryResponse } from "../lib/api";
|
||||
import { getStoredToken } from "../lib/auth";
|
||||
import { getDashboardSummary, type DashboardSummaryResponse } from "../../src/lib/api";
|
||||
import { clearAuth, getStoredToken, getStoredUser } from "../../src/lib/auth";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const router = useRouter();
|
||||
@@ -17,7 +17,15 @@ export default function DashboardPage() {
|
||||
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 role = typeof storedUser?.role === "string" ? storedUser.role : "";
|
||||
if (!role || ["driver", "سائق"].includes(role)) {
|
||||
clearAuth(); // add this import at top of file
|
||||
router.replace("/login");
|
||||
return;
|
||||
}
|
||||
let mounted = true;
|
||||
|
||||
async function load() {
|
||||
@@ -87,7 +95,7 @@ export default function DashboardPage() {
|
||||
<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">مباشر من /v1/dashboard/summary</span>
|
||||
<span className="rounded-full bg-emerald-400/10 px-3 py-1 text-xs text-emerald-200">مباشر من /dashboard/summary</span>
|
||||
</div>
|
||||
<div className="mt-6 space-y-4">
|
||||
{activeTrips.length ? activeTrips.map((trip) => (
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
const DEFAULT_BASE_URL = "/api/proxy";
|
||||
|
||||
export const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL || DEFAULT_BASE_URL;
|
||||
|
||||
function normalizePath(path: string) {
|
||||
return path.startsWith("/") ? path : `/${path}`;
|
||||
}
|
||||
|
||||
async function requestJson<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
const token = typeof window !== "undefined" ? localStorage.getItem("auth_token") : null;
|
||||
const headers = new Headers(init.headers || {});
|
||||
|
||||
if (!headers.has("Content-Type") && !(init.body instanceof FormData)) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
|
||||
const endpoint = `${API_BASE_URL}${normalizePath(path)}`;
|
||||
|
||||
let attempt = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
...init,
|
||||
headers,
|
||||
cache: "no-store",
|
||||
method: init.method || "GET",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(text || `Request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
} catch (error) {
|
||||
attempt += 1;
|
||||
if (attempt >= maxAttempts) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 300));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Request failed after retries.");
|
||||
}
|
||||
|
||||
export type DashboardSummaryResponse = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
stats: {
|
||||
clients: number;
|
||||
orders: number;
|
||||
trips: number;
|
||||
cars: number;
|
||||
drivers: number;
|
||||
};
|
||||
alerts: {
|
||||
expiringCars: Array<Record<string, unknown>>;
|
||||
expiringDrivers: Array<Record<string, unknown>>;
|
||||
upcomingMaint: Array<Record<string, unknown>>;
|
||||
};
|
||||
activeTrips: Array<{
|
||||
id: string;
|
||||
tripNumber: string;
|
||||
title: string;
|
||||
progress: number;
|
||||
}>;
|
||||
accountSecurity: {
|
||||
lastLogin: string | null;
|
||||
requestMeta: Record<string, unknown> | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export async function getDashboardSummary() {
|
||||
return requestJson<DashboardSummaryResponse>("/v1/dashboard/summary");
|
||||
}
|
||||
|
||||
export async function getHealth() {
|
||||
return requestJson<{ status: string; message: string }>("/health");
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import { API_BASE_URL } from "./api";
|
||||
|
||||
const AUTH_TOKEN_KEY = "auth_token";
|
||||
const AUTH_USER_KEY = "auth_user";
|
||||
|
||||
export interface AuthUser {
|
||||
id?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
permissions?: string[];
|
||||
roleId?: string;
|
||||
userName?: string;
|
||||
phone?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
user: AuthUser;
|
||||
}
|
||||
|
||||
export function getStoredToken() {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function getStoredUser(): AuthUser | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
|
||||
const raw = localStorage.getItem(AUTH_USER_KEY);
|
||||
if (!raw) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as AuthUser;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveAuth(token: string, user: AuthUser) {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, token);
|
||||
localStorage.setItem(AUTH_USER_KEY, JSON.stringify(user));
|
||||
}
|
||||
|
||||
export function clearAuth() {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||
localStorage.removeItem(AUTH_USER_KEY);
|
||||
}
|
||||
|
||||
export async function loginUser(identity: string, password: string) {
|
||||
const response = await fetch(`${API_BASE_URL}/v1/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ identity, password }),
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => ({}))) as {
|
||||
success?: boolean;
|
||||
message?: string;
|
||||
data?: {
|
||||
token?: string;
|
||||
user?: AuthUser;
|
||||
};
|
||||
token?: string;
|
||||
user?: AuthUser;
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.message || "Login failed. Please check your credentials.");
|
||||
}
|
||||
|
||||
const token = payload.data?.token ?? payload.token;
|
||||
const user = payload.data?.user ?? payload.user;
|
||||
|
||||
if (!token || !user) {
|
||||
throw new Error("The server did not return a valid session payload.");
|
||||
}
|
||||
|
||||
const rolePermissions = (user as { role?: { permissions?: Array<{ permission?: { slug?: string } }> } }).role?.permissions ?? [];
|
||||
const permissions = Array.isArray(rolePermissions)
|
||||
? rolePermissions
|
||||
.map((entry) => entry?.permission?.slug)
|
||||
.filter((slug): slug is string => Boolean(slug))
|
||||
: [];
|
||||
|
||||
saveAuth(token, { ...user, permissions });
|
||||
|
||||
return { token, user: { ...user, permissions } } as LoginResponse;
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { loginUser } from "../lib/auth";
|
||||
import { loginUser } from "../../src/lib/auth";
|
||||
import Logo from "@/src/utils/logo";
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
@@ -19,9 +20,27 @@ export default function LoginPage() {
|
||||
|
||||
try {
|
||||
await loginUser(identity, password);
|
||||
router.replace("/dashboard");
|
||||
|
||||
window.location.href = "/dashboard"; // Use full page reload to ensure all client state is reset after login
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "System temporarily unavailable. Please try later.");
|
||||
// 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.",
|
||||
);
|
||||
} else {
|
||||
setError(err.message);
|
||||
}
|
||||
} else {
|
||||
setError("System temporarily unavailable. Please try later.");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -31,23 +50,46 @@ export default function LoginPage() {
|
||||
<main className="min-h-screen bg-slate-100 text-slate-900">
|
||||
<section className="grid min-h-screen w-full lg:grid-cols-[1fr_1fr]">
|
||||
<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 className="absolute inset-0 opacity-30" style={{ backgroundImage: "radial-gradient(circle, rgba(255,255,255,0.18) 1px, transparent 1px)", backgroundSize: "40px 40px" }} />
|
||||
<div
|
||||
className="absolute inset-0 opacity-30"
|
||||
style={{
|
||||
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>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl border border-white/30 bg-white/20">🚚</div>
|
||||
<span className="text-[22px] font-bold">LogiFlow</span>
|
||||
</div>
|
||||
<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>
|
||||
<Logo />
|
||||
<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>
|
||||
<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>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">{title}</h2>
|
||||
<p className="text-xs text-white/80">{desc}</p>
|
||||
@@ -56,23 +98,32 @@ export default function LoginPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[12px] text-white/50">مصمم لفرق العمليات التي تحتاج إلى الوضوح والسرعة والثقة.</p>
|
||||
<p className="text-[12px] text-white/50">
|
||||
مصمم لفرق العمليات التي تحتاج إلى الوضوح والسرعة والثقة.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<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">
|
||||
<p className="text-[28px] font-bold text-slate-900">مرحبًا بعودتك</p>
|
||||
<p className="mt-1 text-[14px] text-slate-500">سجل الدخول لإدارة عمليات التسليم</p>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="w-full max-w-100 rounded-[14px] border border-slate-200 bg-white p-8 shadow-sm"
|
||||
>
|
||||
<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
|
||||
autoComplete="email"
|
||||
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)}
|
||||
placeholder="name@company.com"
|
||||
placeholder="name@company.com / 05xxxxxxxx / username"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
@@ -91,10 +142,19 @@ export default function LoginPage() {
|
||||
</label>
|
||||
|
||||
<div className="mt-3 flex justify-end">
|
||||
<Link href="/forgot-password" className="text-[12px] text-blue-600 hover:underline">هل نسيت كلمة المرور؟</Link>
|
||||
<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 ? (
|
||||
<p className="mt-4 rounded-[9px] border border-red-200 bg-red-50 p-3 text-[13px] text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
@@ -105,7 +165,10 @@ export default function LoginPage() {
|
||||
</button>
|
||||
|
||||
<p className="mt-4 text-center text-[13px] text-slate-500">
|
||||
ليس لديك حساب؟ <Link href="/register" className="text-blue-600 hover:underline">إنشاء حساب</Link>
|
||||
ليس لديك حساب؟{" "}
|
||||
<Link href="/register" className="text-blue-600 hover:underline">
|
||||
إنشاء حساب
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { loginUser } from "../lib/auth";
|
||||
|
||||
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);
|
||||
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await loginUser(identity, password);
|
||||
router.replace("/dashboard");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "System temporarily unavailable. Please try later.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
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]">
|
||||
<aside className="relative overflow-hidden bg-gradient-to-br from-blue-900 via-blue-600 to-blue-700 px-8 py-10 text-white lg:px-12 lg:py-14">
|
||||
<div className="absolute inset-0 opacity-30" style={{ 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>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl border border-white/30 bg-white/20">🚚</div>
|
||||
<span className="text-[22px] font-bold">LogiFlow</span>
|
||||
</div>
|
||||
<h1 className="mt-10 max-w-md text-[36px] font-bold leading-tight tracking-[-0.5px]">Logistics made simple, delivery made certain.</h1>
|
||||
<p className="mt-4 max-w-md text-[15px] leading-7 text-white/75">Monitor deliveries, coordinate drivers, and keep every order visible from one secure sign-in.</p>
|
||||
<div className="mt-8 space-y-3">
|
||||
{[
|
||||
["Live dispatch", "Track every order from pickup to delivery in one place."],
|
||||
["Secure access", "Role-aware sessions keep client and admin routes separate."],
|
||||
["Fast operations", "Use the dashboard to review status, alerts, and trip movement."],
|
||||
].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>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold">{title}</h2>
|
||||
<p className="text-xs text-white/80">{desc}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[12px] text-white/50">Built for operations teams that need clarity, speed, and confidence.</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<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-[400px] rounded-[14px] border border-slate-200 bg-white p-8 shadow-sm">
|
||||
<p className="text-[28px] font-bold text-slate-900">Welcome back</p>
|
||||
<p className="mt-1 text-[14px] text-slate-500">Sign in to manage your deliveries</p>
|
||||
|
||||
<label className="mt-6 block text-[11px] font-bold uppercase tracking-[0.5px] text-slate-500">
|
||||
Email address
|
||||
<input
|
||||
autoComplete="email"
|
||||
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)}
|
||||
placeholder="name@company.com"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="mt-4 block text-[11px] font-bold uppercase tracking-[0.5px] text-slate-500">
|
||||
Password
|
||||
<input
|
||||
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)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="mt-3 flex justify-end">
|
||||
<Link href="/forgot-password" className="text-[12px] text-blue-600 hover:underline">Forgot password?</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}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="mt-6 inline-flex h-[46px] 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 ? "Signing in…" : "Sign in"}
|
||||
</button>
|
||||
|
||||
<p className="mt-4 text-center text-[13px] text-slate-500">
|
||||
Don't have an account? <Link href="/register" className="text-blue-600 hover:underline">Register</Link>
|
||||
</p>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
13
app/page.tsx
13
app/page.tsx
@@ -1,3 +1,4 @@
|
||||
import Link from "next/link";
|
||||
import Navbar from "./components/layout/Navbar";
|
||||
|
||||
const stats = [
|
||||
@@ -75,8 +76,12 @@ export default function Home() {
|
||||
أدِر كل شحنة وعميل وسائق من منصة واحدة آمنة. مصممة للمشرفين والعملاء والمستخدمين الميدانيين.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button className="h-11 rounded-xl bg-white px-5 text-[13px] font-semibold text-blue-600 shadow-md">إنشاء حساب عميل</button>
|
||||
<button className="h-11 rounded-xl border border-white/30 bg-white/15 px-5 text-[13px] font-semibold text-white shadow-md backdrop-blur">تسجيل الدخول للوحة التحكم</button>
|
||||
<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>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -167,8 +172,8 @@ 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">LogiFlow</div>
|
||||
<div className="text-center text-white/40">© 2026 LogiFlow. جميع الحقوق محفوظة.</div>
|
||||
<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>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user