diff --git a/app/api/proxy/[...path]/route.ts b/app/api/proxy/[...path]/route.ts new file mode 100644 index 0000000..18f6fb2 --- /dev/null +++ b/app/api/proxy/[...path]/route.ts @@ -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); +} diff --git a/app/audit/page.tsx b/app/audit/page.tsx new file mode 100644 index 0000000..c5f1983 --- /dev/null +++ b/app/audit/page.tsx @@ -0,0 +1,3 @@ +export default function AuditPage() { + return
Audit log module is ready for compliance and activity review.
; +} diff --git a/app/branches/page.tsx b/app/branches/page.tsx new file mode 100644 index 0000000..94cc3d6 --- /dev/null +++ b/app/branches/page.tsx @@ -0,0 +1,3 @@ +export default function BranchesPage() { + return
Branches module is ready for the location and hub management table.
; +} diff --git a/app/cars/page.tsx b/app/cars/page.tsx new file mode 100644 index 0000000..0f6ba59 --- /dev/null +++ b/app/cars/page.tsx @@ -0,0 +1,3 @@ +export default function CarsPage() { + return
Cars module is ready for the fleet management dashboard.
; +} diff --git a/app/client/page.tsx b/app/client/page.tsx new file mode 100644 index 0000000..8d0e359 --- /dev/null +++ b/app/client/page.tsx @@ -0,0 +1,81 @@ +"use client"; + +export default function ClientHomePage() { + return ( +
+
+ + +
+
+
+

الصفحة الرئيسية للعميل

+

صباح الخير يا عميل 👋

+

نظرة عامة على عمليات الإرسال والنشاط الأخير للطلبات.

+
+ +
+ +
+ {[ + ['الطلبات النشطة', '12', 'text-blue-600'], + ['المسلمة هذا الشهر', '86', 'text-emerald-600'], + ['العناوين المحفوظة', '4', 'text-slate-900'], + ].map(([label, value, tone]) => ( +
+

{label}

+

{value}

+
+ ))} +
+ +
+
+

الطلب النشط

+
+
+
+

ORD-202606-00142

+

المركز الشمالي → مخزن وسط المدينة

+
+ في الطريق +
+
+
+
+
+
+ +
+

الطلبات الأخيرة

+
    + {[ + ['ORD-202606-00110', 'تم التوصيل', 'تم التوصيل', 'green'], + ['ORD-202606-00115', 'ملغى', 'ملغى', 'red'], + ].map(([ref, status, note, tone]) => ( +
  • +
    +

    {ref}

    +

    {note}

    +
    + {status} +
  • + ))} +
+
+
+
+
+
+ ); +} diff --git a/app/clients/page.tsx b/app/clients/page.tsx new file mode 100644 index 0000000..462fd72 --- /dev/null +++ b/app/clients/page.tsx @@ -0,0 +1,3 @@ +export default function ClientsPage() { + return
Clients module is ready for the address and order management views.
; +} diff --git a/app/components/layout/Navbar.tsx b/app/components/layout/Navbar.tsx new file mode 100644 index 0000000..8bb8ff0 --- /dev/null +++ b/app/components/layout/Navbar.tsx @@ -0,0 +1,61 @@ + + + +const navLinks = [ + { label: "الرئيسية", active: true }, + { label: "المميزات" }, + { label: "كيف يعمل" }, + { label: "الأسعار" }, + { label: "الأسئلة الشائعة" }, +]; + +export default function Navbar() { + return ( + + ); +} \ No newline at end of file diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx new file mode 100644 index 0000000..2fee0d8 --- /dev/null +++ b/app/components/layout/Sidebar.tsx @@ -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 ( + + ); +} diff --git a/app/components/layout/Topbar.tsx b/app/components/layout/Topbar.tsx new file mode 100644 index 0000000..40d810c --- /dev/null +++ b/app/components/layout/Topbar.tsx @@ -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 ( +
+
+
+

Operations dashboard

+

Logistics delivery management

+
+ +
+ Overview +
Manager
+ +
+
+
+ ); +} diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx new file mode 100644 index 0000000..6f7e2a5 --- /dev/null +++ b/app/dashboard/layout.tsx @@ -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 ( +
+
+ +
+ +
{children}
+
+
+
+ ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..b2f42f7 --- /dev/null +++ b/app/dashboard/page.tsx @@ -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(null); + const [error, setError] = useState(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 ( +
+
+
+

لوحة العمليات

+
+
+

ذكاء الأسطول في لمحة سريعة

+

هذه الشاشة متوافقة مع نقطة ملخص لوحة الإدارة في الخلفية وتوفر عرضًا واضحًا لطلب الأسطول، وتنبيهات السلامة، وتقدم الرحلات.

+
+ العودة إلى بوابة العميل +
+
+ + {loading &&

جارٍ تحميل مقاييس الخلفية…

} + {error &&

{error}

} + + {!loading && !error && ( + <> +
+ {summaryCards.map((item) => ( +
+
+

{item.label}

+

{item.value}

+
+ ))} +
+ +
+
+
+
+

الرحلات النشطة

+

تقدم الرحلات

+
+ مباشر من /v1/dashboard/summary +
+
+ {activeTrips.length ? activeTrips.map((trip) => ( +
+
+
+

{trip.tripNumber}

+

{trip.title}

+
+ {trip.progress}% +
+
+
+
+
+ )) :

لا توجد رحلات في-progress حالياً.

} +
+
+ +
+

تنبيهات الالتزام

+

التجديدات القادمة

+
+ {(alerts?.expiringCars || []).slice(0, 3).map((item, index) => ( +
{String((item as Record).message || "Vehicle expiry alert")}
+ ))} + {(alerts?.expiringDrivers || []).slice(0, 3).map((item, index) => ( +
{String((item as Record).message || "Driver expiry alert")}
+ ))} + {(alerts?.upcomingMaint || []).slice(0, 3).map((item, index) => ( +
{String((item as Record).message || "Maintenance alert")}
+ ))} +
+
+
+ +
+
+

الأمان

+

لمحة الحساب

+
+
آخر تسجيل دخول: {data?.data?.accountSecurity?.lastLogin || "لم يتم العثور على حدث تسجيل دخول"}
+
بيانات الجهاز: {data?.data?.accountSecurity?.requestMeta ? JSON.stringify(data.data.accountSecurity.requestMeta) : "غير متاح"}
+
+
+ +
+

خريطة الخلفية

+

النقاط النهائية المستندة إلى Swagger

+
    +
  • /v1/dashboard/summary — نظرة عامة تشغيلية
  • +
  • /v1/client — إدارة العملاء
  • +
  • /v1/orders — دورة شحن الطلبات
  • +
  • /v1/trip — تنظيم الرحلات
  • +
+
+
+ + )} +
+
+ ); +} diff --git a/app/drivers/page.tsx b/app/drivers/page.tsx new file mode 100644 index 0000000..514ddb0 --- /dev/null +++ b/app/drivers/page.tsx @@ -0,0 +1,3 @@ +export default function DriversPage() { + return
Drivers module is ready for the workforce and report tools.
; +} diff --git a/app/forbidden/page.tsx b/app/forbidden/page.tsx new file mode 100644 index 0000000..b3c885b --- /dev/null +++ b/app/forbidden/page.tsx @@ -0,0 +1,12 @@ +export default function ForbiddenPage() { + return ( +
+
+

Access denied

+

You do not have permission to open this section.

+

Contact an administrator to request access to the required module.

+ Return to dashboard +
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index 394f799..d66e747 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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); } diff --git a/app/home/page.tsx b/app/home/page.tsx new file mode 100644 index 0000000..687292b --- /dev/null +++ b/app/home/page.tsx @@ -0,0 +1,8 @@ +import { Topbar } from "../components/layout/Topbar"; + +export default function HomePage() { + return
Home + + +
; +} diff --git a/app/layout.tsx b/app/layout.tsx index 976eb90..4def342 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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({ diff --git a/app/lib/api.ts b/app/lib/api.ts new file mode 100644 index 0000000..dacd061 --- /dev/null +++ b/app/lib/api.ts @@ -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(path: string, init: RequestInit = {}): Promise { + 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>; + expiringDrivers: Array>; + upcomingMaint: Array>; + }; + activeTrips: Array<{ + id: string; + tripNumber: string; + title: string; + progress: number; + }>; + accountSecurity: { + lastLogin: string | null; + requestMeta: Record | null; + }; + }; +}; + +export async function getDashboardSummary() { + return requestJson("/v1/dashboard/summary"); +} + +export async function getHealth() { + return requestJson<{ status: string; message: string }>("/health"); +} diff --git a/app/lib/auth.ts b/app/lib/auth.ts new file mode 100644 index 0000000..2c6a36f --- /dev/null +++ b/app/lib/auth.ts @@ -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; +} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..0a40b78 --- /dev/null +++ b/app/login/page.tsx @@ -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(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(event: React.FormEvent) { + 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 ( +
+
+ + +
+
+

مرحبًا بعودتك

+

سجل الدخول لإدارة عمليات التسليم

+ + + + + +
+ هل نسيت كلمة المرور؟ +
+ + {error ?

{error}

: null} + + + +

+ ليس لديك حساب؟ إنشاء حساب +

+
+
+
+
+ ); +} diff --git a/app/login/page.tsx.new b/app/login/page.tsx.new new file mode 100644 index 0000000..da29008 --- /dev/null +++ b/app/login/page.tsx.new @@ -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(null); + const [loading, setLoading] = useState(false); + + async function handleSubmit(event: React.FormEvent) { + 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 ( +
+
+ + +
+
+

Welcome back

+

Sign in to manage your deliveries

+ + + + + +
+ Forgot password? +
+ + {error ?

{error}

: null} + + + +

+ Don't have an account? Register +

+
+
+
+
+ ); +} diff --git a/app/orders/page.tsx b/app/orders/page.tsx new file mode 100644 index 0000000..ca4253b --- /dev/null +++ b/app/orders/page.tsx @@ -0,0 +1,3 @@ +export default function OrdersPage() { + return
Orders module is ready for the order lifecycle and transfer workflows.
; +} diff --git a/app/page.tsx b/app/page.tsx index 3f36f7c..8e18d64 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+
+ + +
+
+
+
+ + + منصة مباشرة • 3 صلاحيات + +

+ إدارة لوجستية سهلة، توصيل مضمون في كل مرة. +

+

+ أدِر كل شحنة وعميل وسائق من منصة واحدة آمنة. مصممة للمشرفين والعملاء والمستخدمين الميدانيين. +

+
+ + +
+
+ +
-
+ +
+
+ {stats.map((item) => ( +
+

{item.label}

+

{item.value}

+
+ ))}
-
-
+ +
+

المميزات الأساسية

+

كل ما يحتاجه فريق اللوجستيك

+

من إدارة الطلبات إلى التتبع المباشر، كل شيء في مكان واحد.

+
+ {features.map((feature) => ( +
+
+ {feature.icon === "truck" ? : feature.icon === "clock" ? : } +
+

{feature.title}

+

{feature.text}

+
+ ))} +
+
+ + +
+
+
+

أدوار المستخدمين

+

منصة واحدة، ثلاثة مداخل

+
+
+ {roles.map((role) => ( +
+ {role.tag} +

{role.title}

+
    + {role.items.map((item) =>
  • {item}
  • )} +
+
+ ))} +
+
+
+ +
+
+

الأسئلة الشائعة

+

أسئلة متكررة

+
+ {faqs.map((faq) => ( +
+

{faq.q}

+

{faq.a}

+
+ ))} +
+
+
+ +
+
+
LogiFlow
+
© 2026 LogiFlow. جميع الحقوق محفوظة.
+
الخصوصية · الشروط · الحالة
+
+
+
); } diff --git a/app/register/page.tsx b/app/register/page.tsx new file mode 100644 index 0000000..2aaac2b --- /dev/null +++ b/app/register/page.tsx @@ -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>({}); + const [loading, setLoading] = useState(false); + + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + const phonePattern = /^\+?[0-9]{10,15}$/; + + function validateStep1() { + const nextErrors: Record = {}; + 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 10–15 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 ( +
+
+
+
+

تسجيل العميل

+

إنشاء حساب LogiFlow

+
+ العودة لتسجيل الدخول +
+ +
+ {progressSteps.map(([index, label, done]) => ( +
+
+ {index} +
+ {label} + {index < 3 ? : null} +
+ ))} +
+ +
+ {step === 1 ? ( +
+
+

المعلومات الشخصية

+

أكمل الحقول المطلوبة للانتقال إلى خطوة التأكيد.

+
+
+ + + + +
+