create all proj components also build main screen ui also create sambil dashbord

This commit is contained in:
m7amedez5511
2026-06-04 21:24:01 +03:00
parent 54e57ff32a
commit 5554f5238c
27 changed files with 1274 additions and 73 deletions

View File

@@ -0,0 +1,65 @@
import { NextRequest, NextResponse } from "next/server";
const BACKEND_BASE_URL = "https://logiapi.slash.sa/api";
function buildHeaders(request: NextRequest) {
const headers = new Headers();
request.headers.forEach((value, key) => {
if (key === "host" || key === "content-length") return;
headers.set(key, value);
});
headers.set("x-forwarded-host", request.headers.get("host") || "localhost");
headers.delete("origin");
return headers;
}
async function proxy(request: NextRequest, params: { path: string[] }) {
const path = params.path?.join("/") ?? "";
const targetUrl = `${BACKEND_BASE_URL}/${path}${request.nextUrl.search}`;
const isBodyless = request.method === "GET" || request.method === "HEAD";
const body = isBodyless ? undefined : await request.text();
const upstream = await fetch(targetUrl, {
method: request.method,
headers: buildHeaders(request),
body,
cache: "no-store",
});
const contentType = upstream.headers.get("content-type") || "application/json";
const responseBody = contentType.includes("application/json")
? await upstream.json().catch(() => null)
: await upstream.text();
return new NextResponse(typeof responseBody === "string" ? responseBody : JSON.stringify(responseBody), {
status: upstream.status,
headers: {
"content-type": contentType,
"cache-control": "no-store",
},
});
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
return proxy(request, await params);
}
export async function POST(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
return proxy(request, await params);
}
export async function PUT(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
return proxy(request, await params);
}
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
return proxy(request, await params);
}
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
return proxy(request, await params);
}

3
app/audit/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function AuditPage() {
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Audit log module is ready for compliance and activity review.</section>;
}

3
app/branches/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function BranchesPage() {
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Branches module is ready for the location and hub management table.</section>;
}

3
app/cars/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
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>;
}

81
app/client/page.tsx Normal file
View File

@@ -0,0 +1,81 @@
"use client";
export default function ClientHomePage() {
return (
<main className="min-h-screen bg-slate-50 text-slate-900">
<section className="flex min-h-screen">
<aside className="hidden w-18 border-r border-slate-200 bg-white lg:flex lg:flex-col lg:items-center lg:py-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-blue-600 text-white">🚚</div>
<nav className="mt-6 flex flex-1 flex-col items-center gap-3 text-[9px] font-semibold text-slate-400">
{['Home','New Order','Track','History','Settings'].map((label, index) => (
<button key={label} type="button" className={`flex h-12 w-12 flex-col items-center justify-center rounded-xl ${index === 0 ? 'bg-blue-50 text-blue-600' : 'bg-transparent hover:bg-slate-100'}`}>
<span className="text-base"></span>
<span>{['الرئيسية','طلب جديد','تتبع','السجل','الإعدادات'][index]}</span>
</button>
))}
</nav>
</aside>
<section className="flex-1 p-6 lg:p-8">
<header className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-[12px] uppercase tracking-[0.35em] text-blue-600">الصفحة الرئيسية للعميل</p>
<h1 className="mt-2 text-[24px] font-bold text-slate-900">صباح الخير يا عميل 👋</h1>
<p className="text-[13px] text-slate-500">نظرة عامة على عمليات الإرسال والنشاط الأخير للطلبات.</p>
</div>
<button className="h-10 rounded-lg bg-blue-600 px-4 text-sm font-semibold text-white">+ طلب جديد</button>
</header>
<div className="mt-6 grid gap-4 md:grid-cols-3">
{[
['الطلبات النشطة', '12', 'text-blue-600'],
['المسلمة هذا الشهر', '86', 'text-emerald-600'],
['العناوين المحفوظة', '4', 'text-slate-900'],
].map(([label, value, tone]) => (
<article key={label} className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
<p className="text-[11px] uppercase tracking-[0.35em] text-slate-400">{label}</p>
<p className={`mt-3 text-[26px] font-bold ${tone}`}>{value}</p>
</article>
))}
</div>
<div className="mt-6 grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<article className="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
<h2 className="text-[16px] font-semibold">الطلب النشط</h2>
<div className="mt-4 rounded-xl border border-slate-200 bg-slate-50 p-4 text-sm">
<div className="flex items-center justify-between gap-3">
<div>
<p className="font-semibold text-slate-900">ORD-202606-00142</p>
<p className="text-[12px] text-slate-500">المركز الشمالي مخزن وسط المدينة</p>
</div>
<span className="rounded-full bg-amber-100 px-3 py-1 text-[11px] font-bold text-amber-800">في الطريق</span>
</div>
<div className="mt-4 h-2 rounded-full bg-slate-200">
<div className="h-2 w-2/3 rounded-full bg-blue-600" />
</div>
</div>
</article>
<article className="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
<h2 className="text-[16px] font-semibold">الطلبات الأخيرة</h2>
<ul className="mt-4 divide-y divide-slate-200">
{[
['ORD-202606-00110', 'تم التوصيل', 'تم التوصيل', 'green'],
['ORD-202606-00115', 'ملغى', 'ملغى', 'red'],
].map(([ref, status, note, tone]) => (
<li key={ref} className="flex items-center justify-between gap-3 py-3 text-sm">
<div>
<p className="font-semibold text-slate-900">{ref}</p>
<p className="text-[12px] text-slate-500">{note}</p>
</div>
<span className={`rounded-full px-3 py-1 text-[11px] font-bold ${tone === 'green' ? 'bg-emerald-100 text-emerald-800' : 'bg-rose-100 text-rose-800'}`}>{status}</span>
</li>
))}
</ul>
</article>
</div>
</section>
</section>
</main>
);
}

3
app/clients/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
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>;
}

View File

@@ -0,0 +1,61 @@
const navLinks = [
{ label: "الرئيسية", active: true },
{ label: "المميزات" },
{ label: "كيف يعمل" },
{ label: "الأسعار" },
{ label: "الأسئلة الشائعة" },
];
export default function Navbar() {
return (
<nav dir="rtl" className="w-full border-b border-slate-200 bg-white text-slate-900 shadow-sm">
<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>
<div className="hidden flex-1 items-center justify-center gap-6 md:flex">
{navLinks.map((link) => (
<a
key={link.label}
href="#"
className={`text-[13px] font-semibold transition hover:text-blue-600 ${
link.active ? "text-blue-600" : "text-slate-500"
}`}
>
{link.label}
</a>
))}
</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">
تسجيل الدخول
</button>
</div>
</div>
</nav>
);
}

View File

@@ -0,0 +1,54 @@
"use client";
import Link from "next/link";
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" },
];
export function Sidebar() {
const permissions = navItems.map((item) => item.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">
<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>
</div>
<nav className="mt-8 space-y-2">
{navItems.map((item) => {
const hasPermission = permissions.includes(item.permission) || item.permission === "read-dashboard";
if (!hasPermission) return null;
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>
);
})}
</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>
<p suppressHydrationWarning className="mt-2 font-semibold">Operations user</p>
<p suppressHydrationWarning className="text-xs text-emerald-100/90">Signed in</p>
</div>
</aside>
);
}

View File

@@ -0,0 +1,37 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { clearAuth } from "../../lib/auth";
export function Topbar() {
const router = useRouter();
function handleLogout() {
clearAuth();
router.replace("/login");
}
return (
<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>
</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>
<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>
</div>
</header>
);
}

16
app/dashboard/layout.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { Sidebar } from "../components/layout/Sidebar";
import { Topbar } from "../components/layout/Topbar";
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>
</div>
);
}

153
app/dashboard/page.tsx Normal file
View File

@@ -0,0 +1,153 @@
"use client";
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";
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);
useEffect(() => {
if (!getStoredToken()) {
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 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" },
],
[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>
</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 && !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>
<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">مباشر من /v1/dashboard/summary</span>
</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}%` }} />
</div>
</div>
)) : <p className="text-sm text-slate-300">لا توجد رحلات في-progress حالياً.</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>
<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>
</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>
);
}

3
app/drivers/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
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>;
}

12
app/forbidden/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
export default function ForbiddenPage() {
return (
<main className="min-h-screen bg-[linear-gradient(135deg,#020617_0%,#111827_45%,#172554_100%)] text-slate-100 flex items-center justify-center px-6 py-10">
<section className="w-full max-w-xl rounded-3xl border border-rose-400/20 bg-slate-900/85 p-8 shadow-2xl shadow-slate-950/30 text-center">
<p className="text-sm uppercase tracking-[0.35em] text-rose-200">Access denied</p>
<h1 className="mt-4 text-3xl font-semibold text-white">You do not have permission to open this section.</h1>
<p className="mt-3 text-slate-300">Contact an administrator to request access to the required module.</p>
<a href="/dashboard" className="mt-6 inline-flex rounded-full bg-cyan-400 px-4 py-2 text-sm font-semibold text-slate-950 hover:bg-cyan-300">Return to dashboard</a>
</section>
</main>
);
}

View File

@@ -1,9 +1,29 @@
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap");
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
--blue: #1A73E8;
--blue-dark: #1557B0;
--blue-light: #EBF3FF;
--green: #34A853;
--green-light: #D1FAE5;
--amber: #F59E0B;
--amber-light: #FEF3C7;
--red: #E53935;
--red-light: #FEE2E2;
--text: #111827;
--text-muted: #6B7280;
--text-hint: #9CA3AF;
--border: #E5E7EB;
--surface: #F9FAFB;
--white: #fff;
--sidebar: 230px;
--radius: 10px;
--radius-sm: 7px;
--font: 'Plus Jakarta Sans', sans-serif;
--mono: 'IBM Plex Mono', monospace;
--background: #ECEEF2;
--foreground: #111827;
}
@theme inline {
@@ -13,15 +33,12 @@
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; font-size: 14px; }
body {
background: var(--background);
margin: 0;
min-height: 100vh;
background: #ECEEF2;
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font);
}

8
app/home/page.tsx Normal file
View File

@@ -0,0 +1,8 @@
import { Topbar } from "../components/layout/Topbar";
export default function HomePage() {
return <section className="">Home
<Topbar/>
</section>;
}

View File

@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Logistics Client Portal",
description: "Modern frontend for the LogisticsApp backend with dashboard and client views.",
};
export default function RootLayout({

90
app/lib/api.ts Normal file
View File

@@ -0,0 +1,90 @@
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");
}

92
app/lib/auth.ts Normal file
View File

@@ -0,0 +1,92 @@
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;
}

115
app/login/page.tsx Normal file
View File

@@ -0,0 +1,115 @@
"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-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="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>
<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>
<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">مصمم لفرق العمليات التي تحتاج إلى الوضوح والسرعة والثقة.</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>
<label className="mt-6 block text-[11px] font-bold uppercase tracking-[0.5px] text-slate-500">
البريد الإلكتروني
<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">
كلمة المرور
<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">هل نسيت كلمة المرور؟</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-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 ? "جاري تسجيل الدخول…" : "تسجيل الدخول"}
</button>
<p className="mt-4 text-center text-[13px] text-slate-500">
ليس لديك حساب؟ <Link href="/register" className="text-blue-600 hover:underline">إنشاء حساب</Link>
</p>
</form>
</section>
</section>
</main>
);
}

115
app/login/page.tsx.new Normal file
View File

@@ -0,0 +1,115 @@
"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&apos;t have an account? <Link href="/register" className="text-blue-600 hover:underline">Register</Link>
</p>
</form>
</section>
</section>
</main>
);
}

3
app/orders/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
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>;
}

View File

@@ -1,65 +1,177 @@
import Image from "next/image";
import Navbar from "./components/layout/Navbar";
const stats = [
{ label: "الطلبات النشطة", value: "48", color: "text-blue-600" },
{ label: "مُسلَّم هذا الشهر", value: "312", color: "text-emerald-600" },
{ label: "العملاء المسجلون", value: "127", color: "text-slate-900" },
];
const features = [
{
icon: "truck",
title: "إدارة الطلبات",
text: "إنشاء الطلبات وتتبعها وتحديثها في الوقت الفعلي.",
tone: "blue",
},
{
icon: "clock",
title: "التتبع المباشر",
text: "راقب تقدم الاستلام والتسليم عبر خريطة مرئية.",
tone: "emerald",
},
{
icon: "users",
title: "صلاحيات حسب الدور",
text: "لوحات تحكم منفصلة للمشرفين والعملاء والمستخدمين.",
tone: "amber",
},
];
const roles = [
{
tone: "dark",
tag: "مشرف",
title: "تحكم كامل",
items: ["عرض جميع طلبات العملاء", "تحديث حالات الطلبات", "إدارة العملاء والسائقين", "سجل التدقيق", "تعيين الرحلات"],
},
{
tone: "blue",
tag: "عميل",
title: "طلباتي",
items: ["إضافة طلبات جديدة", "تتبع الشحنات النشطة", "عرض سجل الطلبات", "إدارة العناوين", "التواصل مع الدعم"],
},
{
tone: "green",
tag: "مستخدم",
title: "وصول ميداني",
items: ["عرض الرحلات المعينة", "تحديث حالة التسليم", "الملف الشخصي", "إعدادات الإشعارات", "تفضيلات التطبيق"],
},
];
const faqs = [
{ q: "كيف أنشئ حساب عميل؟", a: "اضغط على “إنشاء حساب عميل” ثم أكمل بياناتك الأساسية لتفعيل الحساب في دقائق." },
{ q: "ماذا يحدث إن أدخلت كلمة مرور خاطئة؟", a: "ستظهر لك رسالة خطأ واضحة مع خيار إعادة المحاولة أو استرجاع كلمة المرور." },
{ q: "هل بياناتي الشخصية محمية؟", a: "نعم، يتم تشفير البيانات باستخدام AES-256-GCM وإجراءات أمان متقدمة." },
{ q: "هل يمكن للعميل رؤية طلبات عملاء آخرين؟", a: "لا، كل عميل يرى فقط طلباته الخاصة ضمن لوحة التحكم المخصصة له." },
];
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
<main className="min-h-screen bg-[#ECEEF2] text-slate-900" dir="rtl">
<Navbar />
<section className="relative overflow-hidden bg-[linear-gradient(145deg,#0D47A1_0%,#1A73E8_45%,#1565C0_100%)] text-white">
<div className="absolute inset-0 opacity-10 [background-image:radial-gradient(rgba(255,255,255,0.9)_1px,transparent_1px)] [background-size:28px_28px]" />
<div className="relative mx-auto flex max-w-7xl flex-col gap-10 px-6 py-12 lg:flex-row lg:items-center lg:justify-between lg:px-8 lg:py-16">
<article className="max-w-2xl space-y-6">
<span className="inline-flex items-center gap-2 rounded-full border border-white/30 bg-white/10 px-4 py-2 text-[13px] text-white shadow-sm backdrop-blur">
<span className="h-2.5 w-2.5 rounded-full bg-emerald-400" />
منصة مباشرة 3 صلاحيات
</span>
<h1 className="max-w-xl text-[30px] font-extrabold leading-tight text-white lg:text-[38px]">
إدارة لوجستية سهلة، توصيل مضمون في كل مرة.
</h1>
<p className="max-w-xl text-[14px] leading-7 text-white/75">
أدِر كل شحنة وعميل وسائق من منصة واحدة آمنة. مصممة للمشرفين والعملاء والمستخدمين الميدانيين.
</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>
</div>
</article>
<aside className="w-full max-w-md rounded-2xl border border-white/20 bg-white/12 p-5 shadow-2xl shadow-blue-950/30 backdrop-blur-md">
<p className="text-[11px] uppercase tracking-[0.35em] text-white/60">تغذية الطلبات المباشرة</p>
<div className="mt-5 space-y-3">
{[
["SHIP-202606-00142", "في الطريق", "bg-amber-100 text-amber-800"],
["SHIP-202606-00139", "تم التسليم", "bg-emerald-100 text-emerald-800"],
["SHIP-202606-00145", "تم الإنشاء", "bg-blue-100 text-blue-800"],
].map(([code, status, badge]) => (
<div key={code} className="flex items-center justify-between rounded-xl border border-white/10 bg-white/8 px-4 py-3">
<span className="font-mono text-[12px] text-white/90">{code}</span>
<span className={`rounded-full px-3 py-1 text-[11px] font-semibold ${badge}`}>{status}</span>
</div>
))}
</div>
<div className="mt-4 flex items-center justify-between text-[12px] text-white/40">
<span>تم التحديث للتو</span>
<span className="text-emerald-300"> النظام متصل</span>
</div>
</aside>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</section>
<section className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-6 py-10 lg:px-8">
<div className="grid gap-4 md:grid-cols-3">
{stats.map((item) => (
<article key={item.label} className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<p className="text-[11px] uppercase tracking-[0.35em] text-slate-500">{item.label}</p>
<p className={`mt-3 text-[32px] font-extrabold ${item.color}`}>{item.value}</p>
</article>
))}
</div>
</main>
</div>
<article className="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-slate-200 lg:p-8">
<p className="text-[11px] uppercase tracking-[0.35em] text-blue-600">المميزات الأساسية</p>
<h2 className="mt-2 text-[22px] font-extrabold text-slate-900">كل ما يحتاجه فريق اللوجستيك</h2>
<p className="mt-2 text-[14px] text-slate-500">من إدارة الطلبات إلى التتبع المباشر، كل شيء في مكان واحد.</p>
<div className="mt-6 grid gap-4 md:grid-cols-3">
{features.map((feature) => (
<article key={feature.title} className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<div className={`mb-4 flex h-11 w-11 items-center justify-center rounded-xl ${feature.tone === "blue" ? "bg-blue-100 text-blue-600" : feature.tone === "emerald" ? "bg-emerald-100 text-emerald-600" : "bg-amber-100 text-amber-600"}`}>
{feature.icon === "truck" ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5"><path d="M3 7h11l3 4h4l-2 4H8" /><circle cx="9" cy="18" r="1.7" fill="currentColor" stroke="none" /><circle cx="18" cy="18" r="1.7" fill="currentColor" stroke="none" /></svg> : feature.icon === "clock" ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5"><circle cx="12" cy="12" r="8" /><path d="M12 7v5l3 2" /></svg> : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="h-5 w-5"><path d="M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2" /><circle cx="9.5" cy="7" r="3" /><path d="M17 8l2 2 4-4" /></svg>}
</div>
<h3 className="text-[16px] font-bold text-slate-900">{feature.title}</h3>
<p className="mt-2 text-[13px] leading-6 text-slate-500">{feature.text}</p>
</article>
))}
</div>
</article>
</section>
<section className="bg-[#ECEEF2] py-2">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-8 px-6 py-10 lg:px-8">
<article className="text-center">
<p className="text-[11px] uppercase tracking-[0.35em] text-blue-600">أدوار المستخدمين</p>
<h2 className="mt-2 text-[22px] font-extrabold text-slate-900">منصة واحدة، ثلاثة مداخل</h2>
</article>
<div className="grid gap-4 md:grid-cols-3">
{roles.map((role) => (
<article key={role.title} className={`rounded-2xl border p-5 shadow-sm ${role.tone === "dark" ? "border-slate-800 bg-slate-950 text-slate-100" : role.tone === "blue" ? "border-blue-200 bg-blue-50 text-blue-950" : "border-emerald-200 bg-emerald-50 text-emerald-950"}`}>
<span className={`inline-flex rounded-full px-3 py-1 text-[11px] font-semibold ${role.tone === "dark" ? "bg-slate-800 text-slate-200" : role.tone === "blue" ? "bg-blue-100 text-blue-700" : "bg-emerald-100 text-emerald-700"}`}>{role.tag}</span>
<h3 className="mt-3 text-[18px] font-extrabold">{role.title}</h3>
<ul className="mt-4 space-y-2 text-[13px] leading-6">
{role.items.map((item) => <li key={item} className="flex items-start gap-2"> <span className="mt-1 text-[12px]"></span><span>{item}</span></li>)}
</ul>
</article>
))}
</div>
</div>
</section>
<section className="mx-auto w-full max-w-7xl px-6 py-10 lg:px-8">
<article className="rounded-3xl bg-white p-6 shadow-sm ring-1 ring-slate-200 lg:p-8">
<p className="text-[11px] uppercase tracking-[0.35em] text-blue-600">الأسئلة الشائعة</p>
<h2 className="mt-2 text-[22px] font-extrabold text-slate-900">أسئلة متكررة</h2>
<div className="mt-6 space-y-3">
{faqs.map((faq) => (
<article key={faq.q} className="rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
<h3 className="text-[14px] font-bold text-slate-900">{faq.q}</h3>
<p className="mt-2 text-[13px] leading-6 text-slate-500">{faq.a}</p>
</article>
))}
</div>
</article>
</section>
<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="flex items-center gap-4 text-white/40">الخصوصية · الشروط · الحالة</div>
</div>
</footer>
</main>
);
}

146
app/register/page.tsx Normal file
View File

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

3
app/roles/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function RolesPage() {
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Roles and permissions module is ready for RBAC assignment and matrix views.</section>;
}

3
app/trips/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function TripsPage() {
return <section className="rounded-3xl border border-white/10 bg-slate-900/80 p-6 text-slate-100 shadow-2xl shadow-slate-950/20">Trips module is ready for scheduling, status tracking, and manifest generation.</section>;
}

3
app/users/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
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>;
}