diff --git a/app/NewOrder/page.tsx b/app/NewOrder/page.tsx new file mode 100644 index 0000000..ee46c87 --- /dev/null +++ b/app/NewOrder/page.tsx @@ -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 ( + + ); +} + +/* شاشة التحميل */ +function BootstrapScreen() { + return ( +
+
+

جارٍ تحميل بيانات الجلسة…

+
+ ); +} + +export default function ClientOrderPage() { + const [step, setStep] = useState("bootstrap"); + const [session, setSession] = useState(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 ( +
+ + +
+
+ {step === "bootstrap" && } + + {step === "register" && ( + + )} + + {step === "dashboard" && session && ( + + )} +
+ +

+ LogiFlow · آمن ومشفر · المملكة العربية السعودية 🇸🇦 +

+
+
+ ); +} \ No newline at end of file diff --git a/app/clients/page.tsx b/app/clients/page.tsx deleted file mode 100644 index 462fd72..0000000 --- a/app/clients/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -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 index 8bb8ff0..8128aef 100644 --- a/app/components/layout/Navbar.tsx +++ b/app/components/layout/Navbar.tsx @@ -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 ( -
- مباشر من /v1/dashboard/summary + مباشر من /dashboard/summary
{activeTrips.length ? activeTrips.map((trip) => ( diff --git a/app/lib/auth.ts b/app/lib/auth.ts deleted file mode 100644 index 2c6a36f..0000000 --- a/app/lib/auth.ts +++ /dev/null @@ -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; -} diff --git a/app/login/page.tsx b/app/login/page.tsx index 0a40b78..ec2e538 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -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() {
-
-

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

-

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

+ +

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

+

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

@@ -91,10 +142,19 @@ export default function LoginPage() {
- هل نسيت كلمة المرور؟ + + هل نسيت كلمة المرور؟ +
- {error ?

{error}

: null} + {error ? ( +

+ {error} +

+ ) : null}
diff --git a/app/login/page.tsx.new b/app/login/page.tsx.new deleted file mode 100644 index da29008..0000000 --- a/app/login/page.tsx.new +++ /dev/null @@ -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(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/page.tsx b/app/page.tsx index 8e18d64..a3cb58a 100644 --- a/app/page.tsx +++ b/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() { أدِر كل شحنة وعميل وسائق من منصة واحدة آمنة. مصممة للمشرفين والعملاء والمستخدمين الميدانيين.

- - + + إنشاء طلب جديد + + + تسجيل الدخول للوحة التحكم +
@@ -167,8 +172,8 @@ export default function Home() {
-
LogiFlow
-
© 2026 LogiFlow. جميع الحقوق محفوظة.
+
Slash.so
+
© 2026 Slash.so. جميع الحقوق محفوظة.
الخصوصية · الشروط · الحالة
diff --git a/next.config.ts b/next.config.ts index e9ffa30..f94cda8 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,19 @@ +// ───────────────────────────────────────────── +// next.config.ts +// Proxy للـ API عشان نتفادى CORS في المتصفح +// ───────────────────────────────────────────── import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + async rewrites() { + return [ + { + // كل طلب لـ /api/proxy/... يتحول لـ logiapi.slash.sa/api/v1/... + source: "/api/proxy/:path*", + destination: "https://logiapi.slash.sa/api/v1/:path*", + }, + ]; + }, }; -export default nextConfig; +export default nextConfig; \ No newline at end of file diff --git a/src/Components/Admen/admen.tsx b/src/Components/Admen/admen.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/Client/client.tsx b/src/Components/Client/client.tsx new file mode 100644 index 0000000..8768e59 --- /dev/null +++ b/src/Components/Client/client.tsx @@ -0,0 +1,212 @@ +"use client"; + +import React, { useState, type FC, type FormEvent } from "react"; +import { saveSession, type ClientSession } from "@/src/utils/helperFun"; +import Spinner from "../Spinner/spinner"; + + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL!; +console.log("API_BASE:", API_BASE); + //make sure API_BASE ends without slash +// ─── Types ─────────────────────────────────── +interface RegForm { + name: string; + email: string; + phone: string; + companyName: string; +} +interface RegErrors { + name?: string; + email?: string; + phone?: string; +} + + + + +interface InputProps extends React.InputHTMLAttributes { + label: string; + error?: string; + icon?: React.ReactNode; +} +const Input: FC = ({ label, error, icon, className = "", ...rest }) => ( +
+ +
+ {icon &&
{icon}
} + +
+ {error &&

{error}

} +
+); + +interface BtnProps extends React.ButtonHTMLAttributes { + loading?: boolean; +} +const PrimaryBtn: FC = ({ loading, children, className = "", disabled, ...rest }) => ( + +); + +const Alert: FC<{ type?: "error" | "info" | "success"; message: string; onClose?: () => void }> = ({ + type = "error", message, onClose, +}) => { + const styles = { + error: "bg-red-50 border-red-200 text-red-700", + info: "bg-blue-50 border-blue-200 text-blue-700", + success: "bg-emerald-50 border-emerald-200 text-emerald-700", + }; + const icons = { error: "⚠", info: "ℹ", success: "✓" }; + return ( +
+ {icons[type]} + {message} + {onClose && ( + + )} +
+ ); +}; + +// ─── RegisterStep ──────────────────────────── +interface RegisterStepProps { + onSuccess: (s: ClientSession) => void; +} + +const RegisterStep: FC = ({ onSuccess }) => { + const [form, setForm] = useState({ name: "", email: "", phone: "", companyName: "" }); + const [errors, setErrors] = useState({}); + const [loading, setLoading] = useState(false); + const [apiError, setApiError] = useState(""); + + const validate = (): boolean => { + const e: RegErrors = {}; + if (!form.name.trim()) + e.name = "الاسم الكامل مطلوب"; + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) + e.email = "أدخل بريدًا إلكترونيًا صحيحًا"; + if (!/^(\+966|0)?[5][0-9]{8}$/.test(form.phone.replace(/\s/g, ""))) + e.phone = "أدخل رقم هاتف سعودي صحيح (+966 5x xxx xxxx)"; + setErrors(e); + return Object.keys(e).length === 0; + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!validate()) return; + setLoading(true); + setApiError(""); + try { + const res = await fetch(`${API_BASE}/client`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: form.name.trim(), + email: form.email.trim(), + phone: form.phone.trim(), + companyName: form.companyName.trim() || undefined, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.message ?? "فشل التسجيل"); + + // save session and proceed + const session: ClientSession = { + id: data.id ?? data._id, + name: data.name, + email: data.email, + phone: data.phone, + }; + saveSession(session); + onSuccess(session); + } catch (err: unknown) { + setApiError(err instanceof Error ? err.message : "فشل التسجيل، يرجى المحاولة مجددًا."); + } finally { + setLoading(false); + } + }; + + const set = (field: keyof RegForm) => (e: React.ChangeEvent) => { + setForm(p => ({ ...p, [field]: e.target.value })); + if (errors[field as keyof RegErrors]) setErrors(p => ({ ...p, [field]: undefined })); + }; + + return ( +
+
+
+ + عميل جديد +
+

إنشاء حساب جديد

+

أدخل بياناتك للبدء مع Slash.so.

+
+ + {apiError && ( +
+ setApiError("")} /> +
+ )} + +
+ } + /> + } + /> + } + /> + } + /> + +
+ + إنشاء الحساب + + +
+ +
+ ); +}; + +export default RegisterStep; \ No newline at end of file diff --git a/src/Components/Client_Adress/clientAdress.tsx b/src/Components/Client_Adress/clientAdress.tsx new file mode 100644 index 0000000..7638cde --- /dev/null +++ b/src/Components/Client_Adress/clientAdress.tsx @@ -0,0 +1,376 @@ +"use client"; +import React, { useState, useEffect, type FC } from "react"; +import { type ClientSession } from "@/src/utils/helperFun"; +import Spinner from "../Spinner/spinner"; + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL!; + +// ─── Types ─────────────────────────────────── +export interface ClientAddress { + _id: string; + label: string; + branchName: string | null; + contactPerson: { name: string; phone: string }; + details: { + city: string; + district?: string; + street: string; + buildingNo?: string; + unitNo?: string; + zipCode?: string; + }; +} + +interface AddrForm { + label: string; + branchName: string; + contactName: string; + contactPhone: string; + city: string; + district: string; + street: string; + buildingNo: string; + unitNo: string; + zipCode: string; +} + +interface AddrErrors { + city?: string; + street?: string; + zipCode?: string; + contactPhone?: string; +} + + + +interface InputProps extends React.InputHTMLAttributes { + label: string; + error?: string; +} +const Input: FC = ({ label, error, className = "", ...rest }) => ( +
+ + + {error &&

{error}

} +
+); + +interface BtnProps extends React.ButtonHTMLAttributes { + loading?: boolean; +} +const PrimaryBtn: FC = ({ loading, children, className = "", disabled, ...rest }) => ( + +); + +const Alert: FC<{ type?: "error" | "info" | "success"; message: string; onClose?: () => void }> = ({ + type = "error", message, onClose, +}) => { + const styles = { + error: "bg-red-50 border-red-200 text-red-700", + info: "bg-blue-50 border-blue-200 text-blue-700", + success: "bg-emerald-50 border-emerald-200 text-emerald-700", + }; + const icons = { error: "⚠", info: "ℹ", success: "✓" }; + return ( +
+ {icons[type]} + {message} + {onClose && ( + + )} +
+ ); +}; + +// ─── AddressStep ───────────────────────────── +interface AddressStepProps { + session: ClientSession; + onSuccess: () => void; +} + +const LABEL_OPTIONS = ["عام", "المنزل", "المكتب الرئيسي", "المستودع", "الفرع"]; + +const AddressStep: FC = ({ session, onSuccess }) => { + const [savedAddresses, setSavedAddresses] = useState([]); + const [loadingAddrs, setLoadingAddrs] = useState(true); + const [selectedId, setSelectedId] = useState(null); + const [showNew, setShowNew] = useState(false); + const [loading, setLoading] = useState(false); + const [apiError, setApiError] = useState(""); + const [formErrors, setFormErrors] = useState({}); + const [form, setForm] = useState({ + label: "عام", branchName: "", contactName: "", contactPhone: "", + city: "", district: "", street: "", buildingNo: "", unitNo: "", zipCode: "", + }); + + // get saved addresses on mount + useEffect(() => { + (async () => { + try { + const res = await fetch(`${API_BASE}/client/${session.id}/addresses`); + const data = await res.json(); + if (res.ok) { + // api might return either { addresses: [...] } or just [...] + const list: ClientAddress[] = Array.isArray(data) ? data : (data.addresses ?? []); + setSavedAddresses(list); + // auto-select first address if available + if (list.length > 0) setSelectedId(list[0]._id); + } + } catch { + // ignore fetch errors, user can add address manually + setShowNew(true); + } finally { + setLoadingAddrs(false); + } + })(); + }, [session.id]); + + const setF = + (field: keyof AddrForm) => + (e: React.ChangeEvent) => { + setForm(p => ({ ...p, [field]: e.target.value })); + if (formErrors[field as keyof AddrErrors]) + setFormErrors(p => ({ ...p, [field]: undefined })); + }; + + const validateAddress = (): boolean => { + const e: AddrErrors = {}; + if (!form.city.trim()) e.city = "المدينة مطلوبة"; + if (!form.street.trim()) e.street = "الشارع مطلوب"; + if (form.zipCode && !/^\d{5}$/.test(form.zipCode)) + e.zipCode = "الرمز البريدي يجب أن يتكون من 5 أرقام"; + if (form.contactPhone && !/^(\+966|0)?[5][0-9]{8}$/.test(form.contactPhone.replace(/\s/g, ""))) + e.contactPhone = "أدخل رقم جوال سعودي صحيح"; + setFormErrors(e); + return Object.keys(e).length === 0; + }; + + const canProceed = selectedId !== null || showNew; + + const handleNext = async () => { + if (selectedId) { + // existing address selected → proceed without API call + setLoading(true); + await new Promise(r => setTimeout(r, 400)); + setLoading(false); + onSuccess(); + return; + } + if (showNew && !validateAddress()) return; + setLoading(true); + setApiError(""); + try { + const res = await fetch(`${API_BASE}/client/${session.id}/addresses`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + label: form.label, + branchName: form.branchName || null, + contactPerson: { + name: form.contactName, + phone: form.contactPhone, + }, + details: { + city: form.city.trim(), + district: form.district.trim() || undefined, + street: form.street.trim(), + buildingNo: form.buildingNo.trim() || undefined, + unitNo: form.unitNo.trim() || undefined, + zipCode: form.zipCode.trim() || undefined, + }, + }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.message ?? "تعذّر حفظ العنوان"); + onSuccess(); + } catch (err: unknown) { + setApiError(err instanceof Error ? err.message : "تعذّر حفظ العنوان، يرجى المحاولة مجددًا."); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+ + تم إنشاء الحساب +
+

اختر عنوان التوصيل

+

+ العنوان مطلوب قبل تقديم أي طلب. +

+
+ + {apiError && ( +
+ setApiError("")} /> +
+ )} + + {/* loading state */} + {loadingAddrs ? ( +
+ + جارٍ تحميل العناوين… +
+ ) : ( + <> + {/* saved addresses */} + {savedAddresses.length > 0 && ( +
+

العناوين المحفوظة

+
+ {savedAddresses.map(addr => ( + + ))} +
+
+ )} + + {/* new address button */} + + + {/* new address form */} + {showNew && ( +
+

تفاصيل العنوان الجديد

+ +
+
+ + +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+ + + +
+
+ )} + + )} + + {!canProceed && !loadingAddrs && ( +

+ + + + يرجى اختيار عنوان أو إضافة عنوان جديد للمتابعة. +

+ )} + +
+ + + التالي — عرض الطلبات + +
+
+ ); +}; + +export default AddressStep; \ No newline at end of file diff --git a/src/Components/Driver/driver.tsx b/src/Components/Driver/driver.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/Driver_Report/driverReport.tsx b/src/Components/Driver_Report/driverReport.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/Home/home.tsx b/src/Components/Home/home.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/Order/order.tsx b/src/Components/Order/order.tsx new file mode 100644 index 0000000..af00836 --- /dev/null +++ b/src/Components/Order/order.tsx @@ -0,0 +1,260 @@ +"use client"; +// ───────────────────────────────────────────── +// order.tsx +// خطوة 3: لوحة الطلبات — بيانات حقيقية من الـ API +// ───────────────────────────────────────────── +import React, { useState, useEffect, type FC } from "react"; +import { + fmtDate, + fmtAmount, + statusColor, + statusLabel, + clearSession, + type ClientSession, + type OrderStatus, +} from "@/src/utils/helperFun"; +import Spinner from "../Spinner/spinner"; + + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL!; + +// ─── Types ─────────────────────────────────── +export interface Order { + id: string; + createdAt: string; + status: OrderStatus; + totalAmount: number; + description: string; +} + +// ─── Order Tracker ─────────────────────────── +const STATUS_STEPS: OrderStatus[] = ["CREATED", "IN_TRANSIT", "DELIVERED"]; +const STEP_LABELS = ["تم الإنشاء", "قيد التوصيل", "تم التسليم"]; +const STEP_ICONS = [ + + + + , + + + + + + , + + + , +]; + +const OrderTracker: FC<{ status: OrderStatus }> = ({ status }) => { + const idx = STATUS_STEPS.indexOf(status === "CANCELLED" ? "CREATED" : status); + return ( +
+ {STATUS_STEPS.map((s, i) => ( + +
+
+ {STEP_ICONS[i]} +
+ + {STEP_LABELS[i]} + +
+ {i < STATUS_STEPS.length - 1 && ( +
+ )} + + ))} +
+ ); +}; + + + + +// ─── DashboardStep ─────────────────────────── +interface DashboardStepProps { + session: ClientSession; +} + +const DashboardStep: FC = ({ session }) => { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + // جلب الطلبات من الـ API + useEffect(() => { + (async () => { + try { + const res = await fetch(`${API_BASE}/client/${session.id}/orders`); + const data = await res.json(); + if (!res.ok) throw new Error(data.message ?? "فشل جلب الطلبات"); + + // الـ API يُرجع: { orders: [...] } أو مصفوفة مباشرة + const list: Order[] = Array.isArray(data) ? data : (data.orders ?? []); + setOrders(list); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : "تعذّر تحميل الطلبات"); + } finally { + setLoading(false); + } + })(); + }, [session.id]); + + // أحدث طلب نشط (CREATED أو IN_TRANSIT) + const activeOrder = orders.find( + o => o.status === "CREATED" || o.status === "IN_TRANSIT" + ); + const pastOrders = orders.filter(o => o !== activeOrder); + + const summary = { + total: orders.length, + delivered: orders.filter(o => o.status === "DELIVERED").length, + pending: orders.filter(o => o.status === "CREATED" || o.status === "IN_TRANSIT").length, + }; + + return ( +
+ {/* الرأس */} +
+
+
+

لوحة الطلبات

+

+ مرحبًا بعودتك، {session.name} +

+
+
+ + نشط +
+
+
+ + {/* حالة التحميل أو الخطأ */} + {loading ? ( +
+ +

جارٍ تحميل الطلبات…

+
+ ) : error ? ( +
+

{error}

+ +
+ ) : ( + <> + {/* إحصائيات */} +
+ {[ + { label: "إجمالي الطلبات", value: summary.total, color: "text-[#111827]" }, + { label: "مُسلَّمة", value: summary.delivered, color: "text-[#34A853]" }, + { label: "قيد التنفيذ", value: summary.pending, color: "text-[#1A73E8]" }, + ].map(s => ( +
+

{s.label}

+

{s.value}

+
+ ))} +
+ + {/* متتبع الطلب النشط */} + {activeOrder ? ( +
+
+
+

الطلب الحالي

+

{activeOrder.id}

+
+ + {statusLabel(activeOrder.status)} + +
+ +
+ ) : ( +
+

لا يوجد طلب نشط حاليًا.

+
+ )} + + {/* جدول الطلبات السابقة */} + {pastOrders.length > 0 && ( +
+
+

سجل الطلبات

+ {pastOrders.length} طلبات +
+
+ + + + {["رقم الطلب", "التاريخ", "الوصف", "الحالة", "المبلغ"].map(h => ( + + ))} + + + + {pastOrders.map((order, i) => ( + + + + + + + + ))} + +
{h}
+ {order.id} + {fmtDate(order.createdAt)}{order.description} + + + {statusLabel(order.status)} + + + {fmtAmount(order.totalAmount)} +
+
+
+ )} + + )} + + {/* مسح الجلسة */} + +
+ ); +}; + +export default DashboardStep; \ No newline at end of file diff --git a/src/Components/Spinner/spinner.tsx b/src/Components/Spinner/spinner.tsx new file mode 100644 index 0000000..a627fc4 --- /dev/null +++ b/src/Components/Spinner/spinner.tsx @@ -0,0 +1,10 @@ +import { FC } from "react"; + +const Spinner: FC = () => ( + + + + +); + +export default Spinner; \ No newline at end of file diff --git a/src/Components/Trip/tripe.tsx b/src/Components/Trip/tripe.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/Trip_Report/tripe_report.tsx b/src/Components/Trip_Report/tripe_report.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/User/user.tsx b/src/Components/User/user.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Components/car/car.tsx b/src/Components/car/car.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/lib/api.ts b/src/lib/api.ts similarity index 61% rename from app/lib/api.ts rename to src/lib/api.ts index dacd061..2789ec4 100644 --- a/app/lib/api.ts +++ b/src/lib/api.ts @@ -7,8 +7,14 @@ 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; +// ─── API REQUESTS ───────────────────────────── +export 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)) { @@ -34,24 +40,30 @@ async function requestJson(path: string, init: RequestInit = {}): Promise }); if (!response.ok) { - const text = await response.text(); - throw new Error(text || `Request failed with status ${response.status}`); + // try to parse error message from response, fallback to status text + const json = await response.json().catch(() => null); + const message = + json?.message || + json?.error || + `Request failed with status ${response.status}`; + throw new Error(message); } return (await response.json()) as T; } catch (error) { attempt += 1; - if (attempt >= maxAttempts) { - throw error; - } - - await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 300)); + if (attempt >= maxAttempts) throw error; + // exponential back-off: 600ms, 1200ms + await new Promise((resolve) => + setTimeout(resolve, 2 ** attempt * 300), + ); } } throw new Error("Request failed after retries."); } +// ─── Types ─────────────────────────────────── export type DashboardSummaryResponse = { success: boolean; message: string; @@ -81,10 +93,11 @@ export type DashboardSummaryResponse = { }; }; +// ─── API calls ─────────────────────────────── export async function getDashboardSummary() { - return requestJson("/v1/dashboard/summary"); + return requestJson("/dashboard/summary"); } export async function getHealth() { return requestJson<{ status: string; message: string }>("/health"); -} +} \ No newline at end of file diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..352ac24 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,138 @@ +import { requestJson } from "./api"; + +const AUTH_TOKEN_KEY = "auth_token"; +const AUTH_USER_KEY = "auth_user"; +const AUTH_TOKEN_COOKIE = "auth_token"; // cookie name must match what the server expects (e.g., in middleware) + +// ─── Cookie helpers ─────────────────────────── +function setTokenCookie(token: string) { + if (typeof document === "undefined") return; + const exp = new Date(Date.now() + 7 * 864e5).toUTCString(); // 7 أيام + document.cookie = `${AUTH_TOKEN_COOKIE}=${encodeURIComponent(token)}; expires=${exp}; path=/; SameSite=Lax`; +} + +function deleteTokenCookie() { + if (typeof document === "undefined") return; + document.cookie = `${AUTH_TOKEN_COOKIE}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; +} + +// ─── Types ─────────────────────────────────── +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; +} + +// ─── Storage ───────────────────────────────── +export function getStoredToken(): string | null { + 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; + //save in localStorage for client-side access + localStorage.setItem(AUTH_TOKEN_KEY, token); + localStorage.setItem(AUTH_USER_KEY, JSON.stringify(user)); + // save in cookie for server-side access (e.g., in middleware) + setTokenCookie(token); +} + +export function clearAuth() { + if (typeof window === "undefined") return; + localStorage.removeItem(AUTH_TOKEN_KEY); + localStorage.removeItem(AUTH_USER_KEY); + deleteTokenCookie(); +} + +// ─── Blocked Roles ──────────────────────────── +const BLOCKED_ROLES = ["driver", "سائق"] as const; + +type RawLoginPayload = { + success?: boolean; + message?: string; + data?: { token?: string; user?: AuthUser }; + token?: string; + user?: AuthUser; +}; + +type RawUser = { + role?: { + name?: string; + permissions?: Array<{ permission?: { slug?: string } }>; + } | string; +}; + +// ─── Login ──────────────────────────────────── +export async function loginUser( + identity: string, + password: string, +): Promise { + const payload = await requestJson("/auth/login", { + method: "POST", + body: JSON.stringify({ identity, password }), + }); + console.log("Login response payload:", payload); + + const token = payload.data?.token ?? payload.token; + const user = payload.data?.user ?? payload.user; + + if (!token || !user) { + throw new Error("اسم المستخدم أو كلمة المرور غير صحيحة."); + } + + // destracture role from user and determine roleName + const rawRole = (user as unknown as RawUser).role; + const roleName: string | undefined = + typeof rawRole === "string" + ? rawRole + : typeof rawRole === "object" && rawRole !== null + ? rawRole.name + : undefined; + + // reject if role is in blocked list + if (roleName && (BLOCKED_ROLES as readonly string[]).includes(roleName)) { + throw new Error("غير مصرح لك بالوصول إلى هذه اللوحة."); + } + + //fetch permissions + const rolePermissions = + typeof rawRole === "object" && rawRole !== null + ? rawRole.permissions ?? [] + : []; + + const permissions = Array.isArray(rolePermissions) + ? rolePermissions + .map((entry) => entry?.permission?.slug) + .filter((slug): slug is string => Boolean(slug)) + : []; + + const fullUser: AuthUser = { ...user, role: roleName, permissions }; + + // save auth data in both localStorage and cookie for client and server access + saveAuth(token, fullUser); + + return { token, user: fullUser }; +} \ No newline at end of file diff --git a/src/utils/helperFun.ts b/src/utils/helperFun.ts new file mode 100644 index 0000000..ae6078a --- /dev/null +++ b/src/utils/helperFun.ts @@ -0,0 +1,84 @@ +// ─── fexed ─────────────────────────────────── +export const SESSION_KEY = "lf_client"; +export const SESSION_COOKIE = "lf_client_id"; + +// ─── Types ─────────────────────────────────── +export interface ClientSession { + id: string; + name: string; + email: string; + phone: string; +} + +// ─── Cookies ───────────────────────────────── +export function getCookie(name: string): string | null { + if (typeof document === "undefined") return null; + const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`)); + return match ? decodeURIComponent(match[2]) : null; +} + +export function setCookie(name: string, value: string, days = 30) { + const exp = new Date(Date.now() + days * 864e5).toUTCString(); + document.cookie = `${name}=${encodeURIComponent(value)}; expires=${exp}; path=/`; +} + +export function deleteCookie(name: string) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; +} + +// ─── Session ───────────────────────────────── +export function loadSession(): ClientSession | null { + try { + const raw = localStorage.getItem(SESSION_KEY); + if (raw) return JSON.parse(raw) as ClientSession; + } catch {} + const id = getCookie(SESSION_COOKIE); + if (id) return { id, name: "", email: "", phone: "" }; + return null; +} + +export function saveSession(s: ClientSession) { + localStorage.setItem(SESSION_KEY, JSON.stringify(s)); + setCookie(SESSION_COOKIE, s.id); +} + +export function clearSession() { + localStorage.removeItem(SESSION_KEY); + deleteCookie(SESSION_COOKIE); +} + +// ─── Formatting ────────────────────────────── +export function fmtDate(iso: string) { + return new Date(iso).toLocaleDateString("ar-SA", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +export function fmtAmount(n: number) { + return `${n.toFixed(2)} ر.س`; +} + +// ─── Order Status ────────────────────────────── +export type OrderStatus = "CREATED" | "IN_TRANSIT" | "DELIVERED" | "CANCELLED"; + +export function statusColor(status: OrderStatus) { + const map: Record = { + CREATED: "bg-blue-50 text-blue-700 border-blue-200", + IN_TRANSIT: "bg-amber-50 text-amber-700 border-amber-200", + DELIVERED: "bg-emerald-50 text-emerald-700 border-emerald-200", + CANCELLED: "bg-red-50 text-red-600 border-red-200", + }; + return map[status]; +} + +export function statusLabel(status: OrderStatus) { + const map: Record = { + CREATED: "تم الإنشاء", + IN_TRANSIT: "قيد التوصيل", + DELIVERED: "تم التسليم", + CANCELLED: "ملغي", + }; + return map[status]; +} \ No newline at end of file diff --git a/src/utils/logo.tsx b/src/utils/logo.tsx new file mode 100644 index 0000000..330a035 --- /dev/null +++ b/src/utils/logo.tsx @@ -0,0 +1,17 @@ +import { FC } from "react"; + +const Logo: FC = () => ( +
+
+ + + + + + +
+ Slash.sa +
+); + +export default Logo; \ No newline at end of file