add archive endpoint to user , driver , car ,order ,trip and update trip fails also update saidecar

This commit is contained in:
m7amedez5511
2026-07-02 19:04:51 +03:00
parent c33778012a
commit d225291b70
29 changed files with 2096 additions and 432 deletions

View File

@@ -7,8 +7,7 @@ import { getStoredUser } from "@/src/lib/auth";
import Logo from "@/src/utils/logo";
import BrandIcon from "@/src/utils/brandIcon";
//ــــــــــــــ Drawer Context
//ــــــــــــــ Drawer Context
const DrawerCtx = createContext<{
open: boolean;
@@ -35,26 +34,82 @@ const navSections = [
{
label: "الرئيسية",
items: [
{ href: "/dashboard", label: "Dashboard", icon: "ti-layout-dashboard", permission: "read-dashboard" },
{ href: "/dashboard/users", label: "Users", icon: "ti-users", permission: "read-user" },
{
href: "/dashboard",
label: "لوحة التحكم",
icon: "ti-layout-dashboard",
permission: "read-dashboard",
},
{
href: "/dashboard/users",
label: "المستخدمون",
icon: "ti-users",
permission: "read-user",
},
],
},
{
label: "الأسطول",
items: [
{ href: "/dashboard/cars", label: "Cars", icon: "ti-car", permission: "read-car" },
{ href: "/dashboard/drivers", label: "Drivers", icon: "ti-steering-wheel", permission: "read-driver" },
{ href: "/dashboard/trips", label: "Trips", icon: "ti-route", permission: "read-trip" },
{
href: "/dashboard/cars",
label: "السيارات",
icon: "ti-car",
permission: "read-car",
},
{
href: "/dashboard/cars_maintenance",
label: "صيانة السيارات",
icon: "ti-tool",
permission: "read-maintenance",
},
{
href: "/dashboard/drivers",
label: "السائقون",
icon: "ti-steering-wheel",
permission: "read-driver",
},
{
href: "/dashboard/trips",
label: "الرحلات",
icon: "ti-route",
permission: "read-trip",
},
],
},
{
label: "العمليات",
items: [
{ href: "/dashboard/orders", label: "Orders", icon: "ti-package", permission: "read-order" },
{ href: "/dashboard/clients", label: "Clients", icon: "ti-users-group", permission: "read-client" },
{ href: "/dashboard/branches", label: "Branches", icon: "ti-building", permission: "read-branch" },
{ href: "/dashboard/roles", label: "Roles", icon: "ti-shield", permission: "read-role" },
{ href: "/dashboard/audit", label: "Audit", icon: "ti-clipboard-list", permission: "read-audit" },
{
href: "/dashboard/orders",
label: "الطلبات",
icon: "ti-package",
permission: "read-order",
},
{
href: "/dashboard/clients",
label: "العملاء",
icon: "ti-users-group",
permission: "read-client",
},
{
href: "/dashboard/branches",
label: "الفروع",
icon: "ti-building",
permission: "read-branch",
},
{
href: "/dashboard/roles",
label: "الادوار",
icon: "ti-shield",
permission: "read-role",
},
{
href: "/dashboard/audit",
label: "سجل العمليات",
icon: "ti-clipboard-list",
permission: "read-audit",
},
],
},
];
@@ -63,7 +118,6 @@ const navSections = [
Brand Icon
══════════════════════════════════════════ */
export function BrandIconButton({ onClick }: { onClick: () => void }) {
return (
<button
@@ -74,9 +128,12 @@ export function BrandIconButton({ onClick }: { onClick: () => void }) {
background: "transparent",
border: "1px solid var(--color-border)",
borderRadius: 8,
width: 36, height: 36,
width: 36,
height: 36,
padding: 2,
display: "flex", alignItems: "center", justifyContent: "center",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
flexShrink: 0,
}}
@@ -94,16 +151,22 @@ const GRAD = "linear-gradient(175deg, #1E3A8A 0%, #1D4ED8 60%, #1565C0 100%)";
globals.css controls ALL display with !important
══════════════════════════════════════════ */
export function Sidebar() {
const pathname = usePathname();
const user = getStoredUser();
const permissions = user?.permissions ?? navSections.flatMap(s => s.items.map(i => i.permission));
const pathname = usePathname();
const user = getStoredUser();
const permissions =
user?.permissions ??
navSections.flatMap((s) => s.items.map((i) => i.permission));
const { open, setOpen } = useSidebarDrawer();
useEffect(() => { setOpen(false); }, [pathname, setOpen]);
useEffect(() => {
setOpen(false);
}, [pathname, setOpen]);
useEffect(() => {
document.body.style.overflow = open ? "hidden" : "";
return () => { document.body.style.overflow = ""; };
return () => {
document.body.style.overflow = "";
};
}, [open]);
return (
@@ -114,14 +177,19 @@ export function Sidebar() {
suppressHydrationWarning
style={{
background: GRAD,
flexDirection: "column", /* NO display property */
flexDirection: "column" /* NO display property */,
width: "var(--sidebar-width)",
flexShrink: 0,
minHeight: "100vh",
padding: "20px 12px",
}}
>
<SidebarContent pathname={pathname} permissions={permissions} user={user} compact={false} />
<SidebarContent
pathname={pathname}
permissions={permissions}
user={user}
compact={false}
/>
</aside>
{/* ══ 2. Compact rail */}
@@ -131,7 +199,7 @@ export function Sidebar() {
aria-label="التنقل المصغر"
style={{
background: GRAD,
flexDirection: "column", /* NO display property */
flexDirection: "column" /* NO display property */,
alignItems: "center",
position: "fixed",
top: 0,
@@ -144,7 +212,12 @@ export function Sidebar() {
overflowY: "auto",
}}
>
<SidebarContent pathname={pathname} permissions={permissions} user={user} compact={true} />
<SidebarContent
pathname={pathname}
permissions={permissions}
user={user}
compact={true}
/>
</aside>
{/* mobile drower*/}
@@ -156,7 +229,10 @@ export function Sidebar() {
aria-hidden="true"
style={{
position: "fixed",
top: 0, left: 0, right: 0, bottom: 0,
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0,0,0,0.5)",
zIndex: 40,
}}
@@ -170,7 +246,7 @@ export function Sidebar() {
aria-label="قائمة التنقل"
style={{
background: GRAD,
flexDirection: "column", /* NO display property */
flexDirection: "column" /* NO display property */,
position: "fixed",
top: 0,
bottom: 0,
@@ -180,7 +256,7 @@ export function Sidebar() {
padding: "20px 12px",
zIndex: 50,
overflowY: "auto",
transform: open ? "translateX(0)" : "translateX(100%)",
transition: "transform 280ms cubic-bezier(0.4,0,0.2,1)",
}}
@@ -197,19 +273,34 @@ export function Sidebar() {
background: "rgba(255,255,255,0.12)",
border: "none",
borderRadius: 8,
width: 30, height: 30,
display: "flex", alignItems: "center", justifyContent: "center",
width: 30,
height: 30,
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
color: "#fff",
}}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
<path d="M18 6 6 18M6 6l12 12"/>
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
<SidebarContent pathname={pathname} permissions={permissions} user={user} compact={false} />
<SidebarContent
pathname={pathname}
permissions={permissions}
user={user}
compact={false}
/>
</aside>
</>
);
@@ -219,7 +310,10 @@ export function Sidebar() {
SidebarContent
══════════════════════════════════════════ */
function SidebarContent({
pathname, permissions, user, compact,
pathname,
permissions,
user,
compact,
}: {
pathname: string;
permissions: string[];
@@ -228,28 +322,52 @@ function SidebarContent({
}) {
return (
<>
<div style={{ marginBottom: 28, display: "flex", justifyContent: compact ? "center" : "flex-start" }}>
<div
style={{
marginBottom: 28,
display: "flex",
justifyContent: compact ? "center" : "flex-start",
}}
>
{compact ? <BrandIcon size={36} /> : <Logo white />}
</div>
<nav style={{ flex: 1, display: "flex", flexDirection: "column", width: "100%" }}
aria-label="Main navigation">
<nav
style={{
flex: 1,
display: "flex",
flexDirection: "column",
width: "100%",
}}
aria-label="Main navigation"
>
{navSections.map((section) => (
<div key={section.label} style={{ marginBottom: 8 }}>
{!compact && (
<p style={{
fontSize: 9, letterSpacing: "0.14em", textTransform: "uppercase",
color: "rgba(255,255,255,0.45)", padding: "8px 8px 4px",
fontWeight: 600, textAlign: "start", margin: 0,
}}>
<p
style={{
fontSize: 9,
letterSpacing: "0.14em",
textTransform: "uppercase",
color: "rgba(255,255,255,0.45)",
padding: "8px 8px 4px",
fontWeight: 600,
textAlign: "start",
margin: 0,
}}
>
{section.label}
</p>
)}
{section.items.map((item) => {
const allowed = item.permission === "read-dashboard" || permissions.includes(item.permission);
const allowed =
item.permission === "read-dashboard" ||
permissions.includes(item.permission);
if (!allowed) return null;
const active = pathname === item.href
|| (item.href !== "/dashboard" && pathname.startsWith(item.href));
const active =
pathname === item.href ||
(item.href !== "/dashboard" &&
pathname.startsWith(item.href + "/"));
return (
<Link
key={item.href}
@@ -266,15 +384,20 @@ function SidebarContent({
fontSize: 13,
fontWeight: active ? 500 : 400,
color: active ? "#FFFFFF" : "rgba(255,255,255,0.55)",
background: active ? "rgba(255,255,255,0.15)" : "transparent",
background: active
? "rgba(255,255,255,0.15)"
: "transparent",
textDecoration: "none",
transition: "var(--transition-base)",
marginBottom: 2,
textAlign: "start",
}}
>
<i className={`ti ${item.icon}`} aria-hidden="true"
style={{ fontSize: compact ? 18 : 16, flexShrink: 0 }} />
<i
className={`ti ${item.icon}`}
aria-hidden="true"
style={{ fontSize: compact ? 18 : 16, flexShrink: 0 }}
/>
{!compact && item.label}
</Link>
);
@@ -288,27 +411,56 @@ function SidebarContent({
suppressHydrationWarning
title={user?.name ?? user?.userName ?? "—"}
style={{
width: 36, height: 36, borderRadius: "var(--radius-full)",
width: 36,
height: 36,
borderRadius: "var(--radius-full)",
background: "rgba(255,255,255,0.18)",
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: 13, fontWeight: 600, color: "#fff", marginTop: 8,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 13,
fontWeight: 600,
color: "#fff",
marginTop: 8,
}}
>
{(user?.name ?? user?.userName ?? "—").slice(0, 1)}
</div>
) : (
<div style={{
background: "rgba(255,255,255,0.12)", borderRadius: 10,
padding: "10px 12px", marginTop: 8, width: "100%",
}}>
<p suppressHydrationWarning style={{ fontSize: 13, fontWeight: 500, color: "#fff", margin: 0, textAlign: "start" }}>
<div
style={{
background: "rgba(255,255,255,0.12)",
borderRadius: 10,
padding: "10px 12px",
marginTop: 8,
width: "100%",
}}
>
<p
suppressHydrationWarning
style={{
fontSize: 13,
fontWeight: 500,
color: "#fff",
margin: 0,
textAlign: "start",
}}
>
{user?.name ?? user?.userName ?? "—"}
</p>
<p suppressHydrationWarning style={{ fontSize: 11, color: "rgba(255,255,255,0.55)", marginTop: 2, textAlign: "start" }}>
<p
suppressHydrationWarning
style={{
fontSize: 11,
color: "rgba(255,255,255,0.55)",
marginTop: 2,
textAlign: "start",
}}
>
{user?.role ?? "Signed in"}
</p>
</div>
)}
</>
);
}
}

View File

@@ -0,0 +1,7 @@
import React from 'react'
export default function page() {
return (
<div>page</div>
)
}

View File

@@ -17,10 +17,16 @@ function DashShell({ children }: { children: React.ReactNode }) {
if (!el) return;
const w = window.innerWidth;
if (w >= 768 && w < 1024) {
el.style.paddingInlineEnd = "72px";
// #sb-compact is `position: fixed; right: 0` (inline-start
// in RTL), so the content needs its reserved space on
// paddingInlineSTART — not End. End resolved to the wrong
// physical side (left) and let the fixed rail sit on top
// of the content on md screens. Using the CSS var instead
// of a hardcoded "72px" also keeps this in sync with
// --sidebar-width-compact if that token ever changes.
el.style.paddingInlineStart = "var(--sidebar-width-compact)";
} else {
el.style.paddingInlineEnd = "0px";
el.style.paddingInlineStart = "0px";
}
}

View File

@@ -0,0 +1,113 @@
"use client";
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Spinner, EmptyState, Badge, PageHeader } from "@/src/Components/UI";
import { tripService } from "@/src/services/trip.service";
import { getStoredToken } from "@/src/lib/auth";
import type { Trip } from "@/src/types/trip";
import { TRIP_STATUS_MAP } from "@/src/types/trip";
function fmtDateTime(iso?: string | null): string {
if (!iso) return "—";
return new Date(iso).toLocaleString("ar-SA", { dateStyle: "medium", timeStyle: "short" });
}
export default function ArchivedTripDetailPage() {
const params = useParams();
const router = useRouter();
const tripId = params?.tripId as string;
const [trip, setTrip] = useState<Trip | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!tripId) return;
let cancelled = false;
(async () => {
setLoading(true);
setError(null);
try {
const token = getStoredToken();
// Archived trips must go through the archived endpoint —
// the normal /trip/:id endpoint won't return soft-deleted trips.
const res = await tripService.getArchivedById(tripId, token);
if (!cancelled) setTrip((res as unknown as { data: Trip }).data);
} catch {
if (!cancelled) setError("لم يتم العثور على هذه الرحلة في الأرشيف.");
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => { cancelled = true; };
}, [tripId]);
if (loading) {
return (
<div className="flex items-center justify-center gap-3 py-24 text-[var(--color-text-muted)]">
<Spinner size="sm" />
<span className="text-sm">جارٍ التحميل</span>
</div>
);
}
// Edge case: invalid/missing/unreachable trip ID
if (error || !trip) {
return (
<EmptyState
icon="🗄️"
title="الرحلة غير موجودة في الأرشيف"
description={error ?? "تعذّر العثور على رحلة بهذا المعرّف."}
action={
<button
onClick={() => router.push("/dashboard/trips")}
className="text-sm font-semibold text-[var(--color-brand-600)] underline"
>
العودة إلى قائمة الرحلات
</button>
}
/>
);
}
const statusConfig = TRIP_STATUS_MAP[trip.status];
return (
<section className="flex flex-col gap-6 p-6" dir="rtl">
<PageHeader
title={trip.title}
description={`رقم الرحلة: ${trip.tripNumber}`}
backHref="/dashboard/trips"
backLabel="الرحلات"
action={<Badge label="مؤرشفة" color="amber" />}
/>
<div className="rounded-[var(--radius-xl)] border border-[var(--color-border)] bg-[var(--color-surface)] p-6 shadow-[var(--shadow-card)]">
<Badge label={statusConfig.label} color="slate" />
<dl className="mt-4 grid grid-cols-2 gap-4 text-sm">
<div>
<dt className="text-[var(--color-text-muted)]">وقت البدء</dt>
<dd>{fmtDateTime(trip.startTime)}</dd>
</div>
<div>
<dt className="text-[var(--color-text-muted)]">وقت الانتهاء</dt>
<dd>{fmtDateTime(trip.endTime)}</dd>
</div>
<div>
<dt className="text-[var(--color-text-muted)]">تاريخ الأرشفة</dt>
<dd>{fmtDateTime(trip.deletedAt)}</dd>
</div>
{trip.driver && (
<div>
<dt className="text-[var(--color-text-muted)]">السائق</dt>
<dd>{trip.driver.name}</dd>
</div>
)}
</dl>
</div>
</section>
);
}

File diff suppressed because it is too large Load Diff