add archive layout pages to user car driver trip order
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
// app/(dashboard)/cars/page.tsx
|
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Spinner, Alert } from "@/src/Components/UI";
|
import { Spinner, Alert, ArchiveButton } from "@/src/Components/UI";
|
||||||
import { CarFormModal } from "@/src/Components/car/CarFormModal";
|
import { CarFormModal } from "@/src/Components/car/CarFormModal";
|
||||||
import { CarDetailPanel } from "@/src/Components/car/CarDetailPanel";
|
import { CarDetailPanel } from "@/src/Components/car/CarDetailPanel";
|
||||||
import { CarDeleteModal } from "@/src/Components/car/CarDeleteModal";
|
import { CarDeleteModal } from "@/src/Components/car/CarDeleteModal";
|
||||||
|
import { ArchivedCarsModal } from "@/src/Components/car/archive/Archivedcarsmodal";
|
||||||
import { useCars, useCarMutations, useToast } from "@/src/hooks/useCars";
|
import { useCars, useCarMutations, useToast } from "@/src/hooks/useCars";
|
||||||
import { fmtDateShort, isExpiringSoon, STATUS_MAP, INS_MAP } from "@/src/types/car";
|
import { fmtDateShort, isExpiringSoon, STATUS_MAP, INS_MAP } from "@/src/types/car";
|
||||||
import type { Car, CreateCarPayload, ToastMsg, UpdateCarPayload } from "@/src/types/car";
|
import type { Car, CreateCarPayload, ToastMsg, UpdateCarPayload } from "@/src/types/car";
|
||||||
@@ -169,6 +170,7 @@ export default function CarsPage() {
|
|||||||
const [detailId, setDetailId] = useState<string | null>(null);
|
const [detailId, setDetailId] = useState<string | null>(null);
|
||||||
const [formTarget, setFormTarget] = useState<Car | null | false>(false); // false = closed
|
const [formTarget, setFormTarget] = useState<Car | null | false>(false); // false = closed
|
||||||
const [deleteTarget, setDeleteTarget] = useState<Car | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<Car | null>(null);
|
||||||
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||||
|
|
||||||
const { toast, notify } = useToast();
|
const { toast, notify } = useToast();
|
||||||
|
|
||||||
@@ -226,6 +228,10 @@ export default function CarsPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{archiveOpen && (
|
||||||
|
<ArchivedCarsModal onClose={() => setArchiveOpen(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }} dir="rtl">
|
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }} dir="rtl">
|
||||||
|
|
||||||
{/* ── Header ── */}
|
{/* ── Header ── */}
|
||||||
@@ -400,6 +406,9 @@ export default function CarsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Floating button to open the archive browser */}
|
||||||
|
<ArchiveButton onClick={() => setArchiveOpen(true)} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { Alert, Spinner } from "@/src/Components/UI";
|
import { Alert, Spinner, ArchiveButton } from "@/src/Components/UI";
|
||||||
import { DriverFormModal } from "@/src/Components/Driver/DriverFormModal";
|
import { DriverFormModal } from "@/src/Components/Driver/DriverFormModal";
|
||||||
import { DriverDeleteModal } from "@/src/Components/Driver/DriverDeleteModal";
|
import { DriverDeleteModal } from "@/src/Components/Driver/DriverDeleteModal";
|
||||||
|
import { ArchivedDrivers } from "@/src/Components/Driver/archive/ArchivedDrivers";
|
||||||
import { useDrivers } from "@/src/hooks/useDriver";
|
import { useDrivers } from "@/src/hooks/useDriver";
|
||||||
import { CreateDriverPayload, Driver, DRIVER_STATUS_MAP, UpdateDriverPayload } from "@/src/types/driver";
|
import { CreateDriverPayload, Driver, DRIVER_STATUS_MAP, UpdateDriverPayload } from "@/src/types/driver";
|
||||||
import { DriverDetailPanel } from "@/src/Components/Driver/DriverDetailPanel";
|
import { DriverDetailPanel } from "@/src/Components/Driver/DriverDetailPanel";
|
||||||
@@ -74,6 +75,8 @@ export default function DriversPage() {
|
|||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
// Bumped after a successful edit to force the detail panel to re-fetch
|
// Bumped after a successful edit to force the detail panel to re-fetch
|
||||||
const [panelRefreshKey, setPanelRefreshKey] = useState(0);
|
const [panelRefreshKey, setPanelRefreshKey] = useState(0);
|
||||||
|
// Archive browser modal open/closed
|
||||||
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||||
|
|
||||||
// ── Handlers ──────────────────────────────────────────────────────────────
|
// ── Handlers ──────────────────────────────────────────────────────────────
|
||||||
const handleEdit = useCallback((driver: Driver) => {
|
const handleEdit = useCallback((driver: Driver) => {
|
||||||
@@ -418,6 +421,14 @@ export default function DriversPage() {
|
|||||||
onConfirm={handleConfirmDelete}
|
onConfirm={handleConfirmDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── Archive browser modal ── */}
|
||||||
|
{archiveOpen && (
|
||||||
|
<ArchivedDrivers onClose={() => setArchiveOpen(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Floating button to open the archive browser ── */}
|
||||||
|
<ArchiveButton onClick={() => setArchiveOpen(true)} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3,10 +3,11 @@
|
|||||||
|
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { Alert, Spinner, Toast } from "@/src/Components/UI";
|
import { Alert, Spinner, Toast, ArchiveButton } from "@/src/Components/UI";
|
||||||
import { OrderFormModal } from "@/src/Components/Order/Orderformmodal";
|
import { OrderFormModal } from "@/src/Components/Order/OrderFormModal";
|
||||||
import { OrderDeleteModal } from "@/src/Components/Order/Orderdeletemodal";
|
import { OrderDeleteModal } from "@/src/Components/Order/OrderDeleteModal";
|
||||||
import { OrderDetailPanel, ORDER_STATUS_MAP } from "@/src/Components/Order/OrderDetailBanel";
|
import { OrderDetailPanel, ORDER_STATUS_MAP } from "@/src/Components/Order/OrderDetailBanel";
|
||||||
|
import { ArchivedOrdersModal } from "@/src/Components/Order/archive/ArchivedOrdersModal";
|
||||||
import { useOrders } from "@/src/hooks/useOrder";
|
import { useOrders } from "@/src/hooks/useOrder";
|
||||||
import type { CreateOrderPayload, Order, UpdateOrderPayload } from "@/src/types/order";
|
import type { CreateOrderPayload, Order, UpdateOrderPayload } from "@/src/types/order";
|
||||||
|
|
||||||
@@ -81,6 +82,8 @@ export default function OrderComponent() {
|
|||||||
// Bumped after a successful edit/status-change to force the detail panel
|
// Bumped after a successful edit/status-change to force the detail panel
|
||||||
// to re-fetch — same trick as DriversPage's panelRefreshKey.
|
// to re-fetch — same trick as DriversPage's panelRefreshKey.
|
||||||
const [panelRefreshKey, setPanelRefreshKey] = useState(0);
|
const [panelRefreshKey, setPanelRefreshKey] = useState(0);
|
||||||
|
// Archive browser modal open/closed
|
||||||
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||||
|
|
||||||
// ── Handlers ─────────────────────────────────────────────────────────
|
// ── Handlers ─────────────────────────────────────────────────────────
|
||||||
const handleEdit = useCallback((order: Order) => {
|
const handleEdit = useCallback((order: Order) => {
|
||||||
@@ -447,12 +450,20 @@ export default function OrderComponent() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ── Archive browser modal ── */}
|
||||||
|
{archiveOpen && (
|
||||||
|
<ArchivedOrdersModal onClose={() => setArchiveOpen(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Toast: transient success/error feedback for create, update,
|
{/* ── Toast: transient success/error feedback for create, update,
|
||||||
delete, and status-change actions — sourced from useOrders()'
|
delete, and status-change actions — sourced from useOrders()'
|
||||||
`notification` state (see useOrders.ts notify()). This is the
|
`notification` state (see useOrders.ts notify()). This is the
|
||||||
same toast used by the Driver pages via DriverDeleteModal's
|
same toast used by the Driver pages via DriverDeleteModal's
|
||||||
parent and useDriver.ts, kept consistent here. ── */}
|
parent and useDriver.ts, kept consistent here. ── */}
|
||||||
<Toast notification={notification} />
|
<Toast notification={notification} />
|
||||||
|
|
||||||
|
{/* Floating button to open the archive browser */}
|
||||||
|
<ArchiveButton onClick={() => setArchiveOpen(true)} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { tripService } from "@/src/services/trip.service";
|
|||||||
import { getStoredToken } from "@/src/lib/auth";
|
import { getStoredToken } from "@/src/lib/auth";
|
||||||
import { TripFormModal } from "@/src/Components/Trip/Tripformmodal";
|
import { TripFormModal } from "@/src/Components/Trip/Tripformmodal";
|
||||||
import { TripDeleteModal } from "@/src/Components/Trip/Tripdeletemodal";
|
import { TripDeleteModal } from "@/src/Components/Trip/Tripdeletemodal";
|
||||||
import { Spinner } from "@/src/Components/UI";
|
import { ArchivedTripList } from "@/src/Components/Trip/archive/ArchivedTripList";
|
||||||
|
import { Spinner, ArchiveButton } from "@/src/Components/UI";
|
||||||
import { Toast, type ToastNotification } from "@/src/Components/UI/Toast";
|
import { Toast, type ToastNotification } from "@/src/Components/UI/Toast";
|
||||||
import type { Trip, TripStatus, CreateTripPayload, UpdateTripPayload } from "@/src/types/trip";
|
import type { Trip, TripStatus, CreateTripPayload, UpdateTripPayload } from "@/src/types/trip";
|
||||||
import { TRIP_STATUS_MAP } from "@/src/types/trip";
|
import { TRIP_STATUS_MAP } from "@/src/types/trip";
|
||||||
@@ -51,6 +52,67 @@ function StatusBadge({ status }: { status: TripStatus }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Archived trips modal ─────────────────────────────────────────────────────
|
||||||
|
// Lightweight wrapper modal around <ArchivedTripList /> — mirrors the
|
||||||
|
// ArchivedUsersModal pattern used on the users page.
|
||||||
|
|
||||||
|
function ArchivedTripsModal({ onClose }: { onClose: () => void }) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// close on Escape
|
||||||
|
useEffect(() => {
|
||||||
|
const h = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
||||||
|
window.addEventListener("keydown", h);
|
||||||
|
return () => window.removeEventListener("keydown", h);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="archived-trips-title"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
style={{
|
||||||
|
position: "fixed", inset: 0, zIndex: 70,
|
||||||
|
background: "rgba(15,23,42,0.6)", backdropFilter: "blur(4px)",
|
||||||
|
display: "flex", alignItems: "flex-start", justifyContent: "center",
|
||||||
|
padding: "2rem 1rem", overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "100%", maxWidth: 920,
|
||||||
|
background: "var(--color-surface-sunken)",
|
||||||
|
borderRadius: "var(--radius-xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
boxShadow: "0 24px 64px rgba(0,0,0,.2)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* header */}
|
||||||
|
<div dir="rtl" style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
}}>
|
||||||
|
<h2 id="archived-trips-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>
|
||||||
|
أرشيف الرحلات
|
||||||
|
</h2>
|
||||||
|
<button type="button" onClick={onClose} aria-label="إغلاق"
|
||||||
|
style={{ width: 34, height: 34, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", cursor: "pointer", fontSize: 18, color: "var(--color-text-muted)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* body */}
|
||||||
|
<div style={{ padding: "1.5rem" }}>
|
||||||
|
<ArchivedTripList onView={trip => router.push(`/dashboard/trips/${trip.id}`)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Page ─────────────────────────────────────────────────────────────────────
|
// ── Page ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function TripsPage() {
|
export default function TripsPage() {
|
||||||
@@ -93,6 +155,8 @@ export default function TripsPage() {
|
|||||||
const [editTrip, setEditTrip] = useState<Trip | null>(null);
|
const [editTrip, setEditTrip] = useState<Trip | null>(null);
|
||||||
const [deleteTarget, setDeleteTarget] = useState<Trip | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<Trip | null>(null);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
// Archive browser modal open/closed
|
||||||
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||||
|
|
||||||
// ── Create / update — calls tripService directly and notifies explicitly ──
|
// ── Create / update — calls tripService directly and notifies explicitly ──
|
||||||
const handleSubmit = async (
|
const handleSubmit = async (
|
||||||
@@ -146,6 +210,25 @@ export default function TripsPage() {
|
|||||||
إدارة جدولة الرحلات وتتبع حالتها
|
إدارة جدولة الرحلات وتتبع حالتها
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: "0.75rem" }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setArchiveOpen(true)}
|
||||||
|
style={{
|
||||||
|
height: 40, padding: "0 1.25rem",
|
||||||
|
borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)", color: "var(--color-text-secondary)",
|
||||||
|
fontSize: 13, fontWeight: 700, cursor: "pointer",
|
||||||
|
display: "flex", alignItems: "center", gap: 8,
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<rect x="2" y="4" width="20" height="5" rx="1" />
|
||||||
|
<path d="M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9" />
|
||||||
|
<path d="M10 13h4" />
|
||||||
|
</svg>
|
||||||
|
أرشيف الرحلات
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setEditTrip(null); setShowForm(true); }}
|
onClick={() => { setEditTrip(null); setShowForm(true); }}
|
||||||
style={{
|
style={{
|
||||||
@@ -163,6 +246,7 @@ export default function TripsPage() {
|
|||||||
إضافة رحلة
|
إضافة رحلة
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ── Filters ── */}
|
{/* ── Filters ── */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -367,6 +451,9 @@ export default function TripsPage() {
|
|||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{archiveOpen && (
|
||||||
|
<ArchivedTripsModal onClose={() => setArchiveOpen(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Toast ── */}
|
{/* ── Toast ── */}
|
||||||
<Toast notification={notification} onDismiss={dismissNotification} />
|
<Toast notification={notification} onDismiss={dismissNotification} />
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Alert, Toast } from "@/src/Components/UI";
|
import { Alert, Toast, ArchiveButton } from "@/src/Components/UI";
|
||||||
import { UserTable } from "@/src/Components/User/UserTable";
|
import { UserTable } from "@/src/Components/User/UserTable";
|
||||||
import { UserFormModal } from "@/src/Components/User/UserFormModal";
|
import { UserFormModal } from "@/src/Components/User/UserFormModal";
|
||||||
import { UserDetailModal } from "@/src/Components/User/UserDetailModal";
|
import { UserDetailModal } from "@/src/Components/User/UserDetailModal";
|
||||||
import { DeleteConfirmModal } from "@/src/Components/User/DeleteConfirmModal";
|
import { DeleteConfirmModal } from "@/src/Components/User/DeleteConfirmModal";
|
||||||
import { useUsers } from "@/src/hooks/useUser";
|
import { useUsers } from "@/src/hooks/useUser";
|
||||||
import type { User, UserFormData } from "@/src/types/user";
|
import type { User, UserFormData } from "@/src/types/user";
|
||||||
|
import { ArchivedUsersModal } from "@/src/Components/User/archive/Archivedusersmodal";
|
||||||
|
|
||||||
export default function UsersPage() {
|
export default function UsersPage() {
|
||||||
// ── Modal state ─────────────────────────────────────────────────────────────
|
// ── Modal state ─────────────────────────────────────────────────────────────
|
||||||
@@ -16,6 +17,8 @@ export default function UsersPage() {
|
|||||||
const [deleteTarget, setDeleteTarget] = useState<User | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<User | null>(null);
|
||||||
// ID of user whose detail modal is open; null = closed
|
// ID of user whose detail modal is open; null = closed
|
||||||
const [viewUserId, setViewUserId] = useState<string | null>(null);
|
const [viewUserId, setViewUserId] = useState<string | null>(null);
|
||||||
|
// Archive browser modal open/closed
|
||||||
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||||
// Local submitting flag shown in DeleteConfirmModal spinner
|
// Local submitting flag shown in DeleteConfirmModal spinner
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
@@ -84,6 +87,11 @@ export default function UsersPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Archive browser modal */}
|
||||||
|
{archiveOpen && (
|
||||||
|
<ArchivedUsersModal onClose={() => setArchiveOpen(false)} />
|
||||||
|
)}
|
||||||
|
|
||||||
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
||||||
|
|
||||||
{/* ── Page header ── */}
|
{/* ── Page header ── */}
|
||||||
@@ -178,6 +186,9 @@ export default function UsersPage() {
|
|||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Floating button to open the archive browser */}
|
||||||
|
<ArchiveButton onClick={() => setArchiveOpen(true)} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
301
src/Components/Driver/archive/ArchivedDriverDetailModal.tsx
Normal file
301
src/Components/Driver/archive/ArchivedDriverDetailModal.tsx
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Spinner } from "../../UI";
|
||||||
|
import { PhotoCard } from "../DriverPhotos";
|
||||||
|
import { getStoredToken } from "@/src/lib/auth";
|
||||||
|
import { archivedDriverService } from "@/src/services/archive/archivedDriver.service";
|
||||||
|
import {
|
||||||
|
DRIVER_STATUS_MAP,
|
||||||
|
DRIVER_CARD_TYPE_MAP,
|
||||||
|
NATIONAL_ID_TYPE_MAP,
|
||||||
|
} from "@/src/types/driver";
|
||||||
|
import type { ArchivedDriver, DriverStatusHistoryEntry } from "@/src/types/driver";
|
||||||
|
|
||||||
|
interface ArchivedDriverDetailModalProps {
|
||||||
|
driverId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── small helper components ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Renders a single label/value row inside the detail body. */
|
||||||
|
function DetailRow({ label, value }: { label: string; value?: string | null }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: "flex", flexDirection: "column", gap: 4,
|
||||||
|
padding: "0.75rem 0",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.15em", color: "var(--color-text-muted)" }}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 13, fontWeight: 500, color: value ? "var(--color-text-primary)" : "var(--color-text-hint)" }}>
|
||||||
|
{value || "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Avatar({ name }: { name: string }) {
|
||||||
|
const initials = name.trim().split(" ").slice(0, 2).map(w => w[0]).join("").toUpperCase();
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
width: 64, height: 64, borderRadius: "50%",
|
||||||
|
background: "linear-gradient(135deg, #EA580C 0%, #B91C1C 100%)",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
fontSize: 22, fontWeight: 700, color: "#FFF",
|
||||||
|
flexShrink: 0,
|
||||||
|
boxShadow: "0 4px 12px rgba(234,88,12,.3)",
|
||||||
|
}}>
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal showing the full profile of a single archived driver, including
|
||||||
|
* documents/photos and status history. Fetches `getById` and
|
||||||
|
* `getStatusHistory` in parallel on mount.
|
||||||
|
*/
|
||||||
|
export function ArchivedDriverDetailModal({ driverId, onClose }: ArchivedDriverDetailModalProps) {
|
||||||
|
const [driver, setDriver] = useState<ArchivedDriver | null>(null);
|
||||||
|
const [history, setHistory] = useState<DriverStatusHistoryEntry[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// close on Escape
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
||||||
|
window.addEventListener("keydown", handler);
|
||||||
|
return () => window.removeEventListener("keydown", handler);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
// fetch archived driver details + status history on mount
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const token = getStoredToken();
|
||||||
|
const [driverRes, historyRes] = await Promise.all([
|
||||||
|
archivedDriverService.getById(driverId, token),
|
||||||
|
archivedDriverService.getStatusHistory(driverId, token),
|
||||||
|
]);
|
||||||
|
if (!cancelled) {
|
||||||
|
setDriver(driverRes.data);
|
||||||
|
setHistory(historyRes.data ?? []);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setError("تعذّر تحميل بيانات السائق المؤرشف. يرجى المحاولة لاحقاً.");
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [driverId]);
|
||||||
|
|
||||||
|
// ── helpers ───────────────────────────────────────────────────────────────
|
||||||
|
const fmt = (iso?: string | null) =>
|
||||||
|
iso ? new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "long", day: "numeric" }) : null;
|
||||||
|
|
||||||
|
const statusConfig = driver ? DRIVER_STATUS_MAP[driver.status] : null;
|
||||||
|
|
||||||
|
// ── render ────────────────────────────────────────────────────────────────
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="archived-driver-detail-title"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
style={{
|
||||||
|
position: "fixed", inset: 0, zIndex: 55,
|
||||||
|
background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "100%", maxWidth: 560,
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
borderRadius: "var(--radius-2xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
boxShadow: "0 24px 64px rgba(0,0,0,.18)",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex", flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* ── header ── */}
|
||||||
|
<div style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>
|
||||||
|
سائق مؤرشف
|
||||||
|
</p>
|
||||||
|
<h2 id="archived-driver-detail-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||||
|
{driver?.name ?? "عرض السائق"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button" onClick={onClose} aria-label="إغلاق"
|
||||||
|
style={{
|
||||||
|
width: 34, height: 34, borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
cursor: "pointer", fontSize: 18,
|
||||||
|
color: "var(--color-text-muted)",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── body ── */}
|
||||||
|
<div style={{ padding: "1.5rem", overflowY: "auto", maxHeight: "75vh" }}>
|
||||||
|
|
||||||
|
{/* loading */}
|
||||||
|
{loading && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 10, padding: "3rem 0", color: "var(--color-text-muted)" }}>
|
||||||
|
<Spinner size="sm" className="text-blue-600" />
|
||||||
|
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* error */}
|
||||||
|
{!loading && error && (
|
||||||
|
<div style={{
|
||||||
|
padding: "1rem 1.25rem",
|
||||||
|
borderRadius: "var(--radius-lg)",
|
||||||
|
background: "#FEF2F2", border: "1px solid #FECACA",
|
||||||
|
fontSize: 13, color: "#991B1B", fontWeight: 500,
|
||||||
|
textAlign: "center",
|
||||||
|
}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* content */}
|
||||||
|
{!loading && driver && (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 0 }} dir="rtl">
|
||||||
|
|
||||||
|
{/* avatar + name + status row */}
|
||||||
|
<div style={{
|
||||||
|
display: "flex", alignItems: "center", gap: "1rem",
|
||||||
|
padding: "0 0 1.25rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
marginBottom: "0.25rem",
|
||||||
|
}}>
|
||||||
|
<Avatar name={driver.name} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<p style={{ fontSize: 16, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>{driver.name}</p>
|
||||||
|
{driver.userName && (
|
||||||
|
<p style={{ marginTop: 3, fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>
|
||||||
|
@{driver.userName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{statusConfig && (
|
||||||
|
<span style={{
|
||||||
|
marginTop: 8, display: "inline-flex", alignItems: "center", gap: 6,
|
||||||
|
borderRadius: "var(--radius-full)",
|
||||||
|
border: `1px solid ${statusConfig.border}`,
|
||||||
|
background: statusConfig.bg,
|
||||||
|
padding: "0.25rem 0.75rem",
|
||||||
|
fontSize: 12, fontWeight: 600, color: statusConfig.color,
|
||||||
|
}}>
|
||||||
|
<span style={{ width: 6, height: 6, borderRadius: "50%", background: statusConfig.dot }} />
|
||||||
|
{statusConfig.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* core info */}
|
||||||
|
<DetailRow label="رقم الجوال" value={driver.phone} />
|
||||||
|
<DetailRow label="البريد الإلكتروني" value={driver.email} />
|
||||||
|
<DetailRow label="العنوان" value={driver.address} />
|
||||||
|
<DetailRow label="الجنسية" value={driver.nationality} />
|
||||||
|
<DetailRow label="نوع الهوية" value={driver.nationalIdType ? NATIONAL_ID_TYPE_MAP[driver.nationalIdType] : null} />
|
||||||
|
<DetailRow label="رقم الهوية" value={driver.nationalId} />
|
||||||
|
<DetailRow label="رقم GOSI" value={driver.gosiNumber} />
|
||||||
|
<DetailRow label="رقم الرخصة" value={driver.licenseNumber} />
|
||||||
|
<DetailRow label="نوع الرخصة" value={driver.licenseType} />
|
||||||
|
<DetailRow label="انتهاء الرخصة" value={fmt(driver.licenseExpiry)} />
|
||||||
|
<DetailRow label="رقم بطاقة السائق" value={driver.driverCardNumber} />
|
||||||
|
<DetailRow label="نوع بطاقة السائق" value={driver.driverCardType ? DRIVER_CARD_TYPE_MAP[driver.driverCardType] : null} />
|
||||||
|
<DetailRow label="نوع السائق" value={driver.driverType} />
|
||||||
|
<DetailRow label="تاريخ الإنشاء" value={fmt(driver.createdAt)} />
|
||||||
|
<DetailRow label="آخر تحديث" value={fmt(driver.updatedAt)} />
|
||||||
|
|
||||||
|
{/* photos */}
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "1.25rem 0 0.5rem" }}>
|
||||||
|
الصور والمستندات
|
||||||
|
</p>
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
|
||||||
|
<PhotoCard url={driver.photoUrl} label="صورة السائق" />
|
||||||
|
<PhotoCard url={driver.nationalPhotoUrl} label="صورة الهوية" />
|
||||||
|
<PhotoCard url={driver.driverCardPhotoUrl} label="صورة البطاقة" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* status history */}
|
||||||
|
{history.length > 0 && (
|
||||||
|
<>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "1.25rem 0 0.5rem" }}>
|
||||||
|
سجل الحالات
|
||||||
|
</p>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||||
|
{history.map((h) => {
|
||||||
|
const s = DRIVER_STATUS_MAP[h.status] ?? DRIVER_STATUS_MAP.Active;
|
||||||
|
return (
|
||||||
|
<div key={h.id} style={{
|
||||||
|
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||||||
|
borderRadius: "var(--radius-md)", border: `1px solid ${s.border}`,
|
||||||
|
background: s.bg, padding: "0.5rem 0.875rem",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 600, color: s.color }}>{s.label}</span>
|
||||||
|
{h.reason && (
|
||||||
|
<span style={{ fontSize: 11, color: "var(--color-text-muted)", marginRight: 8 }}>
|
||||||
|
— {h.reason}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: 11, color: "var(--color-text-muted)" }}>{fmt(h.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── footer ── */}
|
||||||
|
<div style={{
|
||||||
|
padding: "1rem 1.5rem",
|
||||||
|
borderTop: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
display: "flex", justifyContent: "flex-end",
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
type="button" onClick={onClose}
|
||||||
|
style={{
|
||||||
|
height: 40, padding: "0 1.5rem",
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
fontSize: 13, fontWeight: 600,
|
||||||
|
color: "var(--color-text-secondary)",
|
||||||
|
cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
إغلاق
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
src/Components/Driver/archive/ArchivedDriverTable.tsx
Normal file
141
src/Components/Driver/archive/ArchivedDriverTable.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Spinner } from "../../UI";
|
||||||
|
import { DRIVER_STATUS_MAP } from "@/src/types/driver";
|
||||||
|
import type { ArchivedDriver } from "@/src/types/driver";
|
||||||
|
|
||||||
|
// ── status badge ─────────────────────────────────────────────────────────────
|
||||||
|
function StatusBadge({ status }: { status: ArchivedDriver["status"] }) {
|
||||||
|
const cfg = DRIVER_STATUS_MAP[status] ?? DRIVER_STATUS_MAP.Inactive;
|
||||||
|
return (
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 5, borderRadius: "var(--radius-full)", border: `1px solid ${cfg.border}`, background: cfg.bg, padding: "0.2rem 0.625rem", fontSize: 11, fontWeight: 600, color: cfg.color }}>
|
||||||
|
<span style={{ width: 6, height: 6, borderRadius: "50%", background: cfg.dot }} />
|
||||||
|
{cfg.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── icon button ──────────────────────────────────────────────────────────────
|
||||||
|
function IconBtn({ onClick, title, color, bg, borderColor, children }: { onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<button type="button" title={title} aria-label={title} onClick={e => { e.stopPropagation(); onClick(); }}
|
||||||
|
style={{ width: 32, height: 32, borderRadius: "var(--radius-md)", border: `1px solid ${borderColor}`, background: bg, color, cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", transition: "opacity 150ms" }}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── card / header styles ─────────────────────────────────────────────────────
|
||||||
|
const cardStyle: React.CSSProperties = {
|
||||||
|
borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)", overflow: "hidden", boxShadow: "var(--shadow-card)",
|
||||||
|
};
|
||||||
|
const thStyle: React.CSSProperties = {
|
||||||
|
padding: "0.75rem 1.5rem", fontSize: 11, fontWeight: 700,
|
||||||
|
textTransform: "uppercase", letterSpacing: "0.2em",
|
||||||
|
color: "var(--color-text-muted)", background: "var(--color-surface-muted)",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROW_GRID_COLUMNS = "2fr 1.5fr 1fr 1fr 1fr 100px";
|
||||||
|
|
||||||
|
// ── Props ────────────────────────────────────────────────────────────────────
|
||||||
|
interface ArchivedDriverTableProps {
|
||||||
|
drivers: ArchivedDriver[];
|
||||||
|
loading: boolean;
|
||||||
|
search: string;
|
||||||
|
page: number;
|
||||||
|
pages: number;
|
||||||
|
onView: (driver: ArchivedDriver) => void;
|
||||||
|
onPageChange: (p: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the paginated table of archived drivers, including loading,
|
||||||
|
* empty, and error-free states. Mirrors ArchivedUserTable for consistency.
|
||||||
|
*/
|
||||||
|
export function ArchivedDriverTable({ drivers, loading, search, page, pages, onView, onPageChange }: ArchivedDriverTableProps) {
|
||||||
|
return (
|
||||||
|
<div style={cardStyle}>
|
||||||
|
{/* column headers */}
|
||||||
|
<div dir="rtl" style={{ display: "grid", gridTemplateColumns: ROW_GRID_COLUMNS, ...thStyle }}>
|
||||||
|
<span>الاسم</span>
|
||||||
|
<span>اسم المستخدم</span>
|
||||||
|
<span>الحالة</span>
|
||||||
|
<span style={{ textAlign: "center" }}>تاريخ الإنشاء</span>
|
||||||
|
<span style={{ textAlign: "center" }}>آخر تحديث</span>
|
||||||
|
<span style={{ textAlign: "center" }}>إجراءات</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* loading state */}
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "4rem 0", color: "var(--color-text-muted)" }}>
|
||||||
|
<Spinner size="sm" className="text-blue-600" />
|
||||||
|
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
) : drivers.length === 0 ? (
|
||||||
|
/* empty state */
|
||||||
|
<div style={{ textAlign: "center", padding: "4rem 1rem" }}>
|
||||||
|
<p style={{ fontSize: 13, color: "var(--color-text-muted)" }}>
|
||||||
|
{search ? `لا توجد نتائج لـ "${search}"` : "لا يوجد سائقون في الأرشيف."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* data rows */
|
||||||
|
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
||||||
|
{drivers.map((d, i) => (
|
||||||
|
<li key={d.id} onClick={() => onView(d)} style={{
|
||||||
|
display: "grid", gridTemplateColumns: ROW_GRID_COLUMNS,
|
||||||
|
alignItems: "center", gap: "0.5rem", padding: "0.875rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent",
|
||||||
|
fontSize: 13, cursor: "pointer",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontWeight: 600, color: "var(--color-text-primary)", margin: 0 }}>{d.name}</p>
|
||||||
|
<p style={{ marginTop: 2, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-muted)" }}>{d.phone}</p>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>{d.userName ?? "—"}</span>
|
||||||
|
<StatusBadge status={d.status} />
|
||||||
|
<span style={{ textAlign: "center", fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||||
|
{new Date(d.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })}
|
||||||
|
</span>
|
||||||
|
<span style={{ textAlign: "center", fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||||
|
{new Date(d.updatedAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })}
|
||||||
|
</span>
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", gap: 4 }}>
|
||||||
|
{/* view only — delete/restore not implemented yet, same as users */}
|
||||||
|
<IconBtn title={`عرض ${d.name}`} color="#059669" bg="#ECFDF5" borderColor="#A7F3D0" onClick={() => onView(d)}>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
</IconBtn>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* pagination */}
|
||||||
|
{pages > 1 && (
|
||||||
|
<div dir="rtl" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", borderTop: "1px solid var(--color-border)", padding: "0.875rem 1.5rem" }}>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
|
||||||
|
صفحة <strong style={{ color: "var(--color-text-primary)" }}>{page}</strong> من <strong style={{ color: "var(--color-text-primary)" }}>{pages}</strong>
|
||||||
|
</span>
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
{[
|
||||||
|
{ label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 },
|
||||||
|
{ label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages },
|
||||||
|
].map(btn => (
|
||||||
|
<button key={btn.label} type="button" onClick={btn.action} disabled={btn.disabled}
|
||||||
|
style={{ borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-surface-muted)", padding: "0.375rem 0.875rem", fontSize: 12, color: "var(--color-text-secondary)", cursor: btn.disabled ? "not-allowed" : "pointer", opacity: btn.disabled ? 0.4 : 1, fontFamily: "var(--font-sans)" }}>
|
||||||
|
{btn.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
119
src/Components/Driver/archive/ArchivedDrivers.tsx
Normal file
119
src/Components/Driver/archive/ArchivedDrivers.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Alert } from "../../UI";
|
||||||
|
import { ArchivedDriverTable } from "./ArchivedDriverTable";
|
||||||
|
import { ArchivedDriverDetailModal } from "./ArchivedDriverDetailModal";
|
||||||
|
import { useArchivedDrivers } from "@/src/hooks/archive/useArchivedDrivers";
|
||||||
|
|
||||||
|
interface ArchivedDriversProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top-level modal for browsing archived drivers. Wires the
|
||||||
|
* useArchivedDrivers hook to the table and detail modal, mirroring
|
||||||
|
* ArchivedUsersModal's structure and behavior for drivers.
|
||||||
|
*/
|
||||||
|
export function ArchivedDrivers({ onClose }: ArchivedDriversProps) {
|
||||||
|
const [viewDriverId, setViewDriverId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
drivers, loading, total, pages, error,
|
||||||
|
page, search,
|
||||||
|
setPage, handleSearch, clearError,
|
||||||
|
} = useArchivedDrivers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="archive-driver-modal-title"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
style={{
|
||||||
|
position: "fixed", inset: 0, zIndex: 70,
|
||||||
|
background: "rgba(15,23,42,0.6)", backdropFilter: "blur(4px)",
|
||||||
|
display: "flex", alignItems: "flex-start", justifyContent: "center",
|
||||||
|
padding: "2rem 1rem", overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{viewDriverId && (
|
||||||
|
<ArchivedDriverDetailModal driverId={viewDriverId} onClose={() => setViewDriverId(null)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "100%", maxWidth: 920,
|
||||||
|
background: "var(--color-surface-sunken)",
|
||||||
|
borderRadius: "var(--radius-xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
boxShadow: "0 24px 64px rgba(0,0,0,.2)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* header */}
|
||||||
|
<div dir="rtl" style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>
|
||||||
|
الأرشيف
|
||||||
|
</p>
|
||||||
|
<h2 id="archive-driver-modal-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||||
|
السائقون المؤرشفون
|
||||||
|
<span style={{ marginInlineStart: 8, fontSize: 13, fontWeight: 500, color: "var(--color-text-muted)" }}>
|
||||||
|
({total})
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} aria-label="إغلاق"
|
||||||
|
style={{ width: 34, height: 34, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", cursor: "pointer", fontSize: 18, color: "var(--color-text-muted)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* body */}
|
||||||
|
<div style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
|
{/* search */}
|
||||||
|
<div dir="rtl" style={{ position: "relative", maxWidth: 320 }}>
|
||||||
|
<svg style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", width: 16, height: 16, color: "var(--color-text-hint)", pointerEvents: "none" }}
|
||||||
|
fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
|
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="بحث بالاسم أو الهاتف..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => handleSearch(e.target.value)}
|
||||||
|
dir="rtl"
|
||||||
|
style={{
|
||||||
|
width: "100%", height: 40,
|
||||||
|
paddingRight: 36, paddingLeft: 12,
|
||||||
|
borderRadius: "var(--radius-lg)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
fontSize: 13, outline: "none",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <Alert type="error" message={error} onClose={clearError} />}
|
||||||
|
|
||||||
|
<ArchivedDriverTable
|
||||||
|
drivers={drivers}
|
||||||
|
loading={loading}
|
||||||
|
search={search}
|
||||||
|
page={page}
|
||||||
|
pages={pages}
|
||||||
|
onView={driver => setViewDriverId(driver.id)}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
src/Components/Order/archive/ArchivedOrderDetailModal.tsx
Normal file
85
src/Components/Order/archive/ArchivedOrderDetailModal.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ORDER_STATUS_MAP } from "../OrderDetailBanel";
|
||||||
|
import type { ArchivedOrder } from "@/src/types/order";
|
||||||
|
|
||||||
|
interface ArchivedOrderDetailModalProps {
|
||||||
|
order: ArchivedOrder;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailRow({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", padding: "0.55rem 0", borderBottom: "1px solid var(--color-border)" }}>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--color-text-muted)", fontWeight: 600 }}>{label}</span>
|
||||||
|
<span style={{ fontSize: 13, fontFamily: mono ? "var(--font-mono)" : "var(--font-sans)", color: "var(--color-text-primary)" }}>{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDate(iso?: string | null): string {
|
||||||
|
if (!iso) return "—";
|
||||||
|
return new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "long", day: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArchivedOrderDetailModal({ order, onClose }: ArchivedOrderDetailModalProps) {
|
||||||
|
const s = ORDER_STATUS_MAP[order.currentStatus] ?? ORDER_STATUS_MAP.Created;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div role="dialog" aria-modal="true" aria-labelledby="archived-order-title"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
style={{ position: "fixed", inset: 0, zIndex: 80, background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem" }}>
|
||||||
|
<div onClick={e => e.stopPropagation()}
|
||||||
|
style={{ width: "100%", maxWidth: 480, background: "var(--color-surface)", borderRadius: "var(--radius-2xl)", border: "1px solid var(--color-border)", boxShadow: "0 24px 64px rgba(0,0,0,.18)", overflow: "hidden" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "1.25rem 1.5rem", borderBottom: "1px solid var(--color-border)", background: "var(--color-surface-muted)" }}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>طلب مؤرشف</p>
|
||||||
|
<h2 id="archived-order-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0", fontFamily: "var(--font-mono)" }}>
|
||||||
|
{order.shipmentNumber}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} aria-label="إغلاق"
|
||||||
|
style={{ width: 34, height: 34, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", cursor: "pointer", fontSize: 18, color: "var(--color-text-muted)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: "1.5rem", overflowY: "auto", maxHeight: "70vh" }} dir="rtl">
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 6, borderRadius: "var(--radius-full)", border: `1px solid ${s.border}`, background: s.bg, padding: "0.3rem 0.875rem", fontSize: 12, fontWeight: 700, color: s.color, marginBottom: 12 }}>
|
||||||
|
<span style={{ width: 7, height: 7, borderRadius: "50%", background: s.dot }} />
|
||||||
|
{s.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<DetailRow label="اسم المستلم" value={order.recipientName} />
|
||||||
|
<DetailRow label="رقم الجوال" value={order.recipientPhone} mono />
|
||||||
|
<DetailRow label="الكمية" value={String(order.quantity)} />
|
||||||
|
<DetailRow label="الإجمالي الفرعي" value={order.subTotal != null ? `${order.subTotal.toFixed(2)} ر.س` : "—"} mono />
|
||||||
|
<DetailRow label="تاريخ الإنشاء" value={fmtDate(order.createdAt)} />
|
||||||
|
<DetailRow label="آخر تحديث" value={fmtDate(order.updatedAt)} />
|
||||||
|
|
||||||
|
{order.statusHistory && order.statusHistory.length > 0 && (
|
||||||
|
<div style={{ marginTop: 16, display: "flex", flexDirection: "column", gap: 6 }}>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "0 0 4px" }}>سجل الحالات</p>
|
||||||
|
{order.statusHistory.map(h => {
|
||||||
|
const hs = ORDER_STATUS_MAP[h.status] ?? ORDER_STATUS_MAP.Created;
|
||||||
|
return (
|
||||||
|
<div key={h.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", borderRadius: "var(--radius-md)", border: `1px solid ${hs.border}`, background: hs.bg, padding: "0.5rem 0.875rem" }}>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 600, color: hs.color }}>{hs.label}{h.reason ? ` — ${h.reason}` : ""}</span>
|
||||||
|
<span style={{ fontSize: 11, color: "var(--color-text-muted)" }}>{fmtDate(h.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: "1rem 1.5rem", borderTop: "1px solid var(--color-border)", background: "var(--color-surface-muted)", display: "flex", justifyContent: "flex-end" }}>
|
||||||
|
<button type="button" onClick={onClose}
|
||||||
|
style={{ height: 40, padding: "0 1.5rem", borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)", cursor: "pointer", fontFamily: "var(--font-sans)" }}>
|
||||||
|
إغلاق
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
123
src/Components/Order/archive/ArchivedOrderTable.tsx
Normal file
123
src/Components/Order/archive/ArchivedOrderTable.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Spinner } from "../../UI";
|
||||||
|
import { ORDER_STATUS_MAP } from "../OrderDetailBanel";
|
||||||
|
import type { ArchivedOrder } from "@/src/types/order";
|
||||||
|
|
||||||
|
function fmtAmount(n?: number | null): string {
|
||||||
|
if (n == null) return "—";
|
||||||
|
return `${n.toFixed(2)} ر.س`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardStyle: React.CSSProperties = {
|
||||||
|
borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)", overflow: "hidden", boxShadow: "var(--shadow-card)",
|
||||||
|
};
|
||||||
|
const thStyle: React.CSSProperties = {
|
||||||
|
padding: "0.75rem 1.5rem", fontSize: 11, fontWeight: 700,
|
||||||
|
textTransform: "uppercase", letterSpacing: "0.2em",
|
||||||
|
color: "var(--color-text-muted)", background: "var(--color-surface-muted)",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
};
|
||||||
|
const ROW_GRID = "1.6fr 1.6fr 1fr 1fr 100px";
|
||||||
|
|
||||||
|
interface ArchivedOrderTableProps {
|
||||||
|
orders: ArchivedOrder[];
|
||||||
|
loading: boolean;
|
||||||
|
search: string;
|
||||||
|
page: number;
|
||||||
|
pages: number;
|
||||||
|
onView: (order: ArchivedOrder) => void;
|
||||||
|
onPageChange: (p: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArchivedOrderTable({ orders, loading, search, page, pages, onView, onPageChange }: ArchivedOrderTableProps) {
|
||||||
|
return (
|
||||||
|
<div style={cardStyle}>
|
||||||
|
<div dir="rtl" style={{ display: "grid", gridTemplateColumns: ROW_GRID, ...thStyle }}>
|
||||||
|
<span>رقم الشحنة</span>
|
||||||
|
<span>المستلم</span>
|
||||||
|
<span>المبلغ</span>
|
||||||
|
<span>الحالة</span>
|
||||||
|
<span style={{ textAlign: "center" }}>إجراءات</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "4rem 0", color: "var(--color-text-muted)" }}>
|
||||||
|
<Spinner size="sm" className="text-blue-600" />
|
||||||
|
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
) : orders.length === 0 ? (
|
||||||
|
<div style={{ textAlign: "center", padding: "4rem 1rem" }}>
|
||||||
|
<p style={{ fontSize: 13, color: "var(--color-text-muted)" }}>
|
||||||
|
{search ? `لا توجد نتائج لـ "${search}"` : "لا توجد طلبات في الأرشيف."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
||||||
|
{orders.map((o, i) => {
|
||||||
|
const s = ORDER_STATUS_MAP[o.currentStatus] ?? ORDER_STATUS_MAP.Created;
|
||||||
|
return (
|
||||||
|
<li key={o.id} style={{
|
||||||
|
display: "grid", gridTemplateColumns: ROW_GRID,
|
||||||
|
alignItems: "center", gap: "0.5rem", padding: "0.875rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent",
|
||||||
|
fontSize: 13,
|
||||||
|
}}>
|
||||||
|
<span style={{ fontWeight: 600, fontFamily: "var(--font-mono)", color: "#2563EB" }}>
|
||||||
|
{o.shipmentNumber}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontWeight: 600, color: "var(--color-text-primary)", margin: 0 }}>{o.recipientName}</p>
|
||||||
|
<p style={{ marginTop: 2, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-muted)" }}>{o.recipientPhone}</p>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontWeight: 600, fontFamily: "var(--font-mono)", color: "var(--color-text-primary)" }}>
|
||||||
|
{fmtAmount(o.subTotal)}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
display: "inline-flex", alignItems: "center", gap: 5,
|
||||||
|
borderRadius: "var(--radius-full)", border: `1px solid ${s.border}`,
|
||||||
|
background: s.bg, padding: "0.2rem 0.625rem",
|
||||||
|
fontSize: 11, fontWeight: 600, color: s.color, width: "fit-content",
|
||||||
|
}}>
|
||||||
|
<span style={{ width: 6, height: 6, borderRadius: "50%", background: s.dot }} />
|
||||||
|
{s.label}
|
||||||
|
</span>
|
||||||
|
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||||
|
<button type="button" title={`عرض ${o.shipmentNumber}`} aria-label={`عرض ${o.shipmentNumber}`}
|
||||||
|
onClick={e => { e.stopPropagation(); onView(o); }}
|
||||||
|
style={{ width: 32, height: 32, borderRadius: "var(--radius-md)", border: "1px solid #A7F3D0", background: "#ECFDF5", color: "#059669", cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pages > 1 && (
|
||||||
|
<div dir="rtl" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", borderTop: "1px solid var(--color-border)", padding: "0.875rem 1.5rem" }}>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
|
||||||
|
صفحة <strong style={{ color: "var(--color-text-primary)" }}>{page}</strong> من <strong style={{ color: "var(--color-text-primary)" }}>{pages}</strong>
|
||||||
|
</span>
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
{[
|
||||||
|
{ label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 },
|
||||||
|
{ label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages },
|
||||||
|
].map(btn => (
|
||||||
|
<button key={btn.label} type="button" onClick={btn.action} disabled={btn.disabled}
|
||||||
|
style={{ borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-surface-muted)", padding: "0.375rem 0.875rem", fontSize: 12, color: "var(--color-text-secondary)", cursor: btn.disabled ? "not-allowed" : "pointer", opacity: btn.disabled ? 0.4 : 1, fontFamily: "var(--font-sans)" }}>
|
||||||
|
{btn.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
src/Components/Order/archive/ArchivedOrdersModal.tsx
Normal file
115
src/Components/Order/archive/ArchivedOrdersModal.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Alert } from "../../UI";
|
||||||
|
import { ArchivedOrderTable } from "./ArchivedOrderTable";
|
||||||
|
import { ArchivedOrderDetailModal } from "./ArchivedOrderDetailModal";
|
||||||
|
import { useArchivedOrders } from "@/src/hooks/archive/useArchivedOrders";
|
||||||
|
import type { ArchivedOrder } from "@/src/types/order";
|
||||||
|
|
||||||
|
interface ArchivedOrdersModalProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArchivedOrdersModal({ onClose }: ArchivedOrdersModalProps) {
|
||||||
|
const [viewOrder, setViewOrder] = useState<ArchivedOrder | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
orders, loading, total, pages, error,
|
||||||
|
page, search,
|
||||||
|
setPage, handleSearch, clearError,
|
||||||
|
} = useArchivedOrders();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="archive-orders-modal-title"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
style={{
|
||||||
|
position: "fixed", inset: 0, zIndex: 70,
|
||||||
|
background: "rgba(15,23,42,0.6)", backdropFilter: "blur(4px)",
|
||||||
|
display: "flex", alignItems: "flex-start", justifyContent: "center",
|
||||||
|
padding: "2rem 1rem", overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{viewOrder && (
|
||||||
|
<ArchivedOrderDetailModal order={viewOrder} onClose={() => setViewOrder(null)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "100%", maxWidth: 920,
|
||||||
|
background: "var(--color-surface-sunken)",
|
||||||
|
borderRadius: "var(--radius-xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
boxShadow: "0 24px 64px rgba(0,0,0,.2)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* header */}
|
||||||
|
<div dir="rtl" style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>
|
||||||
|
الأرشيف
|
||||||
|
</p>
|
||||||
|
<h2 id="archive-orders-modal-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||||
|
الطلبات المؤرشفة
|
||||||
|
<span style={{ marginInlineStart: 8, fontSize: 13, fontWeight: 500, color: "var(--color-text-muted)" }}>
|
||||||
|
({total})
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} aria-label="إغلاق"
|
||||||
|
style={{ width: 34, height: 34, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", cursor: "pointer", fontSize: 18, color: "var(--color-text-muted)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* body */}
|
||||||
|
<div style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
|
{/* search */}
|
||||||
|
<div dir="rtl" style={{ position: "relative", maxWidth: 320 }}>
|
||||||
|
<svg style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", width: 16, height: 16, color: "var(--color-text-hint)", pointerEvents: "none" }}
|
||||||
|
fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
|
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="رقم الشحنة أو المستلم..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => handleSearch(e.target.value)}
|
||||||
|
dir="rtl"
|
||||||
|
style={{
|
||||||
|
width: "100%", height: 40,
|
||||||
|
paddingRight: 36, paddingLeft: 12,
|
||||||
|
borderRadius: "var(--radius-lg)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
fontSize: 13, outline: "none",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <Alert type="error" message={error} onClose={clearError} />}
|
||||||
|
|
||||||
|
<ArchivedOrderTable
|
||||||
|
orders={orders}
|
||||||
|
loading={loading}
|
||||||
|
search={search}
|
||||||
|
page={page}
|
||||||
|
pages={pages}
|
||||||
|
onView={order => setViewOrder(order)}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
167
src/Components/Trip/archive/ArchivedTripList.tsx
Normal file
167
src/Components/Trip/archive/ArchivedTripList.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Spinner, Alert, EmptyState, Badge } from "@/src/Components/UI";
|
||||||
|
import { useArchivedTrips } from "@/src/hooks/archive/useArchivedTrips";
|
||||||
|
import { TRIP_STATUS_MAP } from "@/src/types/trip";
|
||||||
|
import type { Trip } from "@/src/types/trip";
|
||||||
|
|
||||||
|
interface ArchivedTripListProps {
|
||||||
|
/** Called when the user clicks a row to view trip details */
|
||||||
|
onView?: (trip: Trip) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Status badge — reuses the existing trip status color map ────────────────
|
||||||
|
function TripStatusBadge({ status }: { status: Trip["status"] }) {
|
||||||
|
const s = TRIP_STATUS_MAP[status];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11px] font-bold"
|
||||||
|
style={{ color: s.color, background: s.bg, border: `1px solid ${s.border}` }}
|
||||||
|
>
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full" style={{ background: s.dot }} />
|
||||||
|
{s.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Date formatting helper ───────────────────────────────────────────────────
|
||||||
|
function fmtDate(iso?: string | null): string {
|
||||||
|
if (!iso) return "—";
|
||||||
|
return new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArchivedTripList
|
||||||
|
* Displays a paginated, searchable list of archived trips.
|
||||||
|
* Fetches data via useArchivedTrips (GET /trip/archived), handles loading,
|
||||||
|
* empty, and error states, and renders each trip with its key details.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <ArchivedTripList onView={(trip) => setViewTripId(trip.id)} />
|
||||||
|
*/
|
||||||
|
export function ArchivedTripList({ onView }: ArchivedTripListProps) {
|
||||||
|
const {
|
||||||
|
trips, loading, total, pages, page, search, error,
|
||||||
|
setPage, handleSearch, clearError,
|
||||||
|
} = useArchivedTrips();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div dir="rtl" className="flex flex-col gap-4">
|
||||||
|
{/* ── Header: count + search ── */}
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<h2 className="text-base font-bold text-[var(--color-text-primary)]">
|
||||||
|
الرحلات المؤرشفة
|
||||||
|
<span className="mr-2 text-[13px] font-medium text-[var(--color-text-muted)]">
|
||||||
|
({total})
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="relative w-full sm:w-72">
|
||||||
|
<svg
|
||||||
|
className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--color-text-hint)]"
|
||||||
|
fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="بحث برقم الرحلة أو العنوان..."
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
className="h-10 w-full rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface)] pr-9 pl-3 text-[13px] text-[var(--color-text-primary)] outline-none focus:border-[var(--color-brand-600)] focus:ring-2 focus:ring-[var(--color-brand-600)]/15"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── API error feedback ── */}
|
||||||
|
{error && <Alert type="error" message={error} onClose={clearError} />}
|
||||||
|
|
||||||
|
{/* ── Card ── */}
|
||||||
|
<div className="overflow-hidden rounded-[var(--radius-xl)] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[var(--shadow-card)]">
|
||||||
|
{/* column headers */}
|
||||||
|
<div className="grid grid-cols-[2fr_1.5fr_1fr_1fr_1fr] gap-2 border-b border-[var(--color-border)] bg-[var(--color-surface-muted)] px-6 py-3 text-[11px] font-bold uppercase tracking-[0.2em] text-[var(--color-text-muted)]">
|
||||||
|
<span>الرحلة</span>
|
||||||
|
<span>الحالة</span>
|
||||||
|
<span className="text-center">البدء</span>
|
||||||
|
<span className="text-center">الانتهاء</span>
|
||||||
|
<span className="text-center">النقد المحصّل</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Loading state ── */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center gap-3 py-16 text-[var(--color-text-muted)]">
|
||||||
|
<Spinner size="sm" />
|
||||||
|
<span className="text-[13px]">جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
) : trips.length === 0 ? (
|
||||||
|
/* ── Empty state ── */
|
||||||
|
<EmptyState
|
||||||
|
icon="🗄️"
|
||||||
|
title="لا توجد رحلات مؤرشفة"
|
||||||
|
description={search ? `لا توجد نتائج لـ "${search}"` : "لم يتم أرشفة أي رحلات بعد."}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
/* ── Data rows ── */
|
||||||
|
<ul className="m-0 list-none p-0">
|
||||||
|
{trips.map((trip, i) => (
|
||||||
|
<li
|
||||||
|
key={trip.id}
|
||||||
|
onClick={() => onView?.(trip)}
|
||||||
|
className={`grid grid-cols-[2fr_1.5fr_1fr_1fr_1fr] items-center gap-2 border-b border-[var(--color-border)] px-6 py-3.5 text-[13px] transition-colors hover:bg-[var(--color-surface-muted)] ${
|
||||||
|
onView ? "cursor-pointer" : ""
|
||||||
|
} ${i % 2 !== 0 ? "bg-[var(--color-surface-muted)]/40" : ""}`}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="m-0 truncate font-semibold text-[var(--color-text-primary)]">{trip.title}</p>
|
||||||
|
<p className="mt-0.5 font-mono text-[11px] text-[var(--color-brand-600)]">{trip.tripNumber}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TripStatusBadge status={trip.status} />
|
||||||
|
</div>
|
||||||
|
<span className="text-center text-[12px] text-[var(--color-text-muted)]">
|
||||||
|
{fmtDate(trip.startTime)}
|
||||||
|
</span>
|
||||||
|
<span className="text-center text-[12px] text-[var(--color-text-muted)]">
|
||||||
|
{fmtDate(trip.endTime)}
|
||||||
|
</span>
|
||||||
|
<span className="text-center font-mono text-[12px] font-semibold text-[var(--color-text-primary)]">
|
||||||
|
{trip.totalCashCollected != null
|
||||||
|
? `${Number(trip.totalCashCollected).toLocaleString("ar-SA")} ر.س`
|
||||||
|
: "—"}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Pagination ── */}
|
||||||
|
{pages > 1 && (
|
||||||
|
<div className="flex items-center justify-between border-t border-[var(--color-border)] px-6 py-3.5">
|
||||||
|
<span className="text-[12px] text-[var(--color-text-muted)]">
|
||||||
|
صفحة <strong className="text-[var(--color-text-primary)]">{page}</strong> من{" "}
|
||||||
|
<strong className="text-[var(--color-text-primary)]">{pages}</strong>
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={page === 1}
|
||||||
|
onClick={() => setPage(Math.max(1, page - 1))}
|
||||||
|
className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3.5 py-1.5 text-[12px] text-[var(--color-text-secondary)] disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
>
|
||||||
|
السابق
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={page === pages}
|
||||||
|
onClick={() => setPage(Math.min(pages, page + 1))}
|
||||||
|
className="rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-muted)] px-3.5 py-1.5 text-[12px] text-[var(--color-text-secondary)] disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
>
|
||||||
|
التالي
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
src/Components/UI/ArchiveButton.tsx
Normal file
61
src/Components/UI/ArchiveButton.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
interface ArchiveButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
label?: string;
|
||||||
|
/** "floating" pins it bottom-right of the viewport; "inline" renders it in normal flow */
|
||||||
|
variant?: "floating" | "inline";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArchiveButton({
|
||||||
|
onClick,
|
||||||
|
label = "الأرشيف",
|
||||||
|
variant = "floating",
|
||||||
|
}: ArchiveButtonProps) {
|
||||||
|
const base: React.CSSProperties = {
|
||||||
|
height: 44,
|
||||||
|
padding: "0 1.25rem",
|
||||||
|
borderRadius: "var(--radius-full)",
|
||||||
|
border: "none",
|
||||||
|
background: "#EA580C", // orange-600, matches existing danger/success accent pattern
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "#FFF",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
boxShadow: "0 4px 14px rgba(234,88,12,.35)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
transition: "transform 150ms, box-shadow 150ms",
|
||||||
|
};
|
||||||
|
|
||||||
|
const floatingStyle: React.CSSProperties = variant === "floating"
|
||||||
|
? {
|
||||||
|
position: "fixed",
|
||||||
|
bottom: 24,
|
||||||
|
insetInlineEnd: 24, // logical property — respects RTL/LTR automatically
|
||||||
|
zIndex: 40,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
style={{ ...base, ...floatingStyle }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.transform = "translateY(-2px)")}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.transform = "translateY(0)")}
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<rect x="2" y="4" width="20" height="5" rx="1" />
|
||||||
|
<path d="M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9" />
|
||||||
|
<path d="M10 13h4" />
|
||||||
|
</svg>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,3 +22,5 @@ export { ConfirmDialog } from "./ConfirmDialog";
|
|||||||
export { PageHeader } from "./PageHeader";
|
export { PageHeader } from "./PageHeader";
|
||||||
export { EmptyState } from "./EmptyState";
|
export { EmptyState } from "./EmptyState";
|
||||||
export { Badge } from "./Badge";
|
export { Badge } from "./Badge";
|
||||||
|
|
||||||
|
export { ArchiveButton } from "./ArchiveButton"
|
||||||
235
src/Components/User/archive/Archiveduserdetailmodal.tsx
Normal file
235
src/Components/User/archive/Archiveduserdetailmodal.tsx
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Spinner } from "../../UI";
|
||||||
|
import { getStoredToken } from "@/src/lib/auth";
|
||||||
|
import { archivedUserService } from "@/src/services/archive/archivedUser.service";
|
||||||
|
import type { ArchivedUser } from "@/src/types/user";
|
||||||
|
|
||||||
|
interface ArchivedUserDetailModalProps {
|
||||||
|
userId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── small helper components ───────────────────────────────────────────────────
|
||||||
|
function DetailRow({ label, value }: { label: string; value?: string | null }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: "flex", flexDirection: "column", gap: 4,
|
||||||
|
padding: "0.75rem 0",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.15em", color: "var(--color-text-muted)" }}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span style={{ fontSize: 13, fontWeight: 500, color: value ? "var(--color-text-primary)" : "var(--color-text-hint)" }}>
|
||||||
|
{value || "—"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge({ active }: { active: boolean }) {
|
||||||
|
return (
|
||||||
|
<span style={{
|
||||||
|
display: "inline-flex", alignItems: "center", gap: 6,
|
||||||
|
borderRadius: "var(--radius-full)",
|
||||||
|
border: active ? "1px solid #BBF7D0" : "1px solid #FECACA",
|
||||||
|
background: active ? "#DCFCE7" : "#FEF2F2",
|
||||||
|
padding: "0.25rem 0.75rem",
|
||||||
|
fontSize: 12, fontWeight: 600,
|
||||||
|
color: active ? "#166534" : "#991B1B",
|
||||||
|
}}>
|
||||||
|
<span style={{ width: 6, height: 6, borderRadius: "50%", background: active ? "#16A34A" : "#DC2626" }} />
|
||||||
|
{active ? "نشط" : "معطل"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Avatar({ name }: { name: string }) {
|
||||||
|
const initials = name.trim().split(" ").slice(0, 2).map(w => w[0]).join("").toUpperCase();
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
width: 64, height: 64, borderRadius: "50%",
|
||||||
|
background: "linear-gradient(135deg, #EA580C 0%, #B91C1C 100%)",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
fontSize: 22, fontWeight: 700, color: "#FFF",
|
||||||
|
flexShrink: 0,
|
||||||
|
boxShadow: "0 4px 12px rgba(234,88,12,.3)",
|
||||||
|
}}>
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── main component ────────────────────────────────────────────────────────────
|
||||||
|
export function ArchivedUserDetailModal({ userId, onClose }: ArchivedUserDetailModalProps) {
|
||||||
|
const [user, setUser] = useState<ArchivedUser | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// close on Escape
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
||||||
|
window.addEventListener("keydown", handler);
|
||||||
|
return () => window.removeEventListener("keydown", handler);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
// fetch archived user details on mount
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const token = getStoredToken();
|
||||||
|
const res = await archivedUserService.getById(userId, token);
|
||||||
|
if (!cancelled) setUser(res.data);
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) setError("تعذّر تحميل بيانات المستخدم المؤرشف. يرجى المحاولة لاحقاً.");
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [userId]);
|
||||||
|
|
||||||
|
// ── helpers ───────────────────────────────────────────────────────────────
|
||||||
|
const fmt = (iso?: string | null) =>
|
||||||
|
iso ? new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "long", day: "numeric" }) : null;
|
||||||
|
|
||||||
|
// ── render ────────────────────────────────────────────────────────────────
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="archived-detail-title"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
style={{
|
||||||
|
position: "fixed", inset: 0, zIndex: 55,
|
||||||
|
background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "100%", maxWidth: 480,
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
borderRadius: "var(--radius-2xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
boxShadow: "0 24px 64px rgba(0,0,0,.18)",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex", flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* ── header ── */}
|
||||||
|
<div style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>
|
||||||
|
مستخدم مؤرشف
|
||||||
|
</p>
|
||||||
|
<h2 id="archived-detail-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||||
|
{user?.name ?? "عرض المستخدم"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button" onClick={onClose} aria-label="إغلاق"
|
||||||
|
style={{
|
||||||
|
width: 34, height: 34, borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
cursor: "pointer", fontSize: 18,
|
||||||
|
color: "var(--color-text-muted)",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── body ── */}
|
||||||
|
<div style={{ padding: "1.5rem", overflowY: "auto", maxHeight: "70vh" }}>
|
||||||
|
|
||||||
|
{/* loading */}
|
||||||
|
{loading && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 10, padding: "3rem 0", color: "var(--color-text-muted)" }}>
|
||||||
|
<Spinner size="sm" className="text-blue-600" />
|
||||||
|
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* error */}
|
||||||
|
{!loading && error && (
|
||||||
|
<div style={{
|
||||||
|
padding: "1rem 1.25rem",
|
||||||
|
borderRadius: "var(--radius-lg)",
|
||||||
|
background: "#FEF2F2", border: "1px solid #FECACA",
|
||||||
|
fontSize: 13, color: "#991B1B", fontWeight: 500,
|
||||||
|
textAlign: "center",
|
||||||
|
}}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* content */}
|
||||||
|
{!loading && user && (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 0 }} dir="rtl">
|
||||||
|
|
||||||
|
{/* avatar + name + status row */}
|
||||||
|
<div style={{
|
||||||
|
display: "flex", alignItems: "center", gap: "1rem",
|
||||||
|
padding: "0 0 1.25rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
marginBottom: "0.25rem",
|
||||||
|
}}>
|
||||||
|
<Avatar name={user.name} />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<p style={{ fontSize: 16, fontWeight: 700, color: "var(--color-text-primary)", margin: 0 }}>{user.name}</p>
|
||||||
|
{user.userName && (
|
||||||
|
<p style={{ marginTop: 3, fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>
|
||||||
|
@{user.userName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<StatusBadge active={user.isActive} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* detail rows */}
|
||||||
|
<DetailRow label="رقم الهاتف" value={user.phone} />
|
||||||
|
<DetailRow label="البريد الإلكتروني" value={user.email} />
|
||||||
|
<DetailRow label="تاريخ الإنشاء" value={fmt(user.createdAt)} />
|
||||||
|
<DetailRow label="آخر تحديث" value={fmt(user.updatedAt)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── footer ── */}
|
||||||
|
<div style={{
|
||||||
|
padding: "1rem 1.5rem",
|
||||||
|
borderTop: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
display: "flex", justifyContent: "flex-end",
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
type="button" onClick={onClose}
|
||||||
|
style={{
|
||||||
|
height: 40, padding: "0 1.5rem",
|
||||||
|
borderRadius: "var(--radius-md)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
fontSize: 13, fontWeight: 600,
|
||||||
|
color: "var(--color-text-secondary)",
|
||||||
|
cursor: "pointer", fontFamily: "var(--font-sans)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
إغلاق
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
114
src/Components/User/archive/Archivedusersmodal.tsx
Normal file
114
src/Components/User/archive/Archivedusersmodal.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Alert } from "../../UI";
|
||||||
|
import { ArchivedUserTable } from "./Archivedusertable";
|
||||||
|
import { ArchivedUserDetailModal } from "./Archiveduserdetailmodal";
|
||||||
|
import { useArchivedUsers } from "@/src/hooks/archive/useArchivedUsers";
|
||||||
|
|
||||||
|
interface ArchivedUsersModalProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArchivedUsersModal({ onClose }: ArchivedUsersModalProps) {
|
||||||
|
const [viewUserId, setViewUserId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
users, loading, total, pages, error,
|
||||||
|
page, search,
|
||||||
|
setPage, handleSearch, clearError,
|
||||||
|
} = useArchivedUsers();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="archive-modal-title"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
style={{
|
||||||
|
position: "fixed", inset: 0, zIndex: 70,
|
||||||
|
background: "rgba(15,23,42,0.6)", backdropFilter: "blur(4px)",
|
||||||
|
display: "flex", alignItems: "flex-start", justifyContent: "center",
|
||||||
|
padding: "2rem 1rem", overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{viewUserId && (
|
||||||
|
<ArchivedUserDetailModal userId={viewUserId} onClose={() => setViewUserId(null)} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "100%", maxWidth: 920,
|
||||||
|
background: "var(--color-surface-sunken)",
|
||||||
|
borderRadius: "var(--radius-xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
boxShadow: "0 24px 64px rgba(0,0,0,.2)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* header */}
|
||||||
|
<div dir="rtl" style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>
|
||||||
|
الأرشيف
|
||||||
|
</p>
|
||||||
|
<h2 id="archive-modal-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||||
|
المستخدمون المؤرشفون
|
||||||
|
<span style={{ marginInlineStart: 8, fontSize: 13, fontWeight: 500, color: "var(--color-text-muted)" }}>
|
||||||
|
({total})
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} aria-label="إغلاق"
|
||||||
|
style={{ width: 34, height: 34, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", cursor: "pointer", fontSize: 18, color: "var(--color-text-muted)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* body */}
|
||||||
|
<div style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
|
{/* search */}
|
||||||
|
<div dir="rtl" style={{ position: "relative", maxWidth: 320 }}>
|
||||||
|
<svg style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", width: 16, height: 16, color: "var(--color-text-hint)", pointerEvents: "none" }}
|
||||||
|
fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
|
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="بحث بالاسم أو الهاتف..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => handleSearch(e.target.value)}
|
||||||
|
dir="rtl"
|
||||||
|
style={{
|
||||||
|
width: "100%", height: 40,
|
||||||
|
paddingRight: 36, paddingLeft: 12,
|
||||||
|
borderRadius: "var(--radius-lg)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
fontSize: 13, outline: "none",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <Alert type="error" message={error} onClose={clearError} />}
|
||||||
|
|
||||||
|
<ArchivedUserTable
|
||||||
|
users={users}
|
||||||
|
loading={loading}
|
||||||
|
search={search}
|
||||||
|
page={page}
|
||||||
|
pages={pages}
|
||||||
|
onView={user => setViewUserId(user.id)}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
134
src/Components/User/archive/Archivedusertable.tsx
Normal file
134
src/Components/User/archive/Archivedusertable.tsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Spinner } from "../../UI";
|
||||||
|
import type { ArchivedUser } from "@/src/types/user";
|
||||||
|
|
||||||
|
// ── status badge ─────────────────────────────────────────────────────────────
|
||||||
|
function StatusBadge({ active }: { active: boolean }) {
|
||||||
|
return (
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 5, borderRadius: "var(--radius-full)", border: active ? "1px solid #BBF7D0" : "1px solid #FECACA", background: active ? "#DCFCE7" : "#FEF2F2", padding: "0.2rem 0.625rem", fontSize: 11, fontWeight: 600, color: active ? "#166534" : "#991B1B" }}>
|
||||||
|
<span style={{ width: 6, height: 6, borderRadius: "50%", background: active ? "#16A34A" : "#DC2626" }} />
|
||||||
|
{active ? "نشط" : "معطل"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── icon button ──────────────────────────────────────────────────────────────
|
||||||
|
function IconBtn({ onClick, title, color, bg, borderColor, children }: { onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<button type="button" title={title} aria-label={title} onClick={e => { e.stopPropagation(); onClick(); }}
|
||||||
|
style={{ width: 32, height: 32, borderRadius: "var(--radius-md)", border: `1px solid ${borderColor}`, background: bg, color, cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", transition: "opacity 150ms" }}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── card / header styles ─────────────────────────────────────────────────────
|
||||||
|
const cardStyle: React.CSSProperties = {
|
||||||
|
borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)", overflow: "hidden", boxShadow: "var(--shadow-card)",
|
||||||
|
};
|
||||||
|
const thStyle: React.CSSProperties = {
|
||||||
|
padding: "0.75rem 1.5rem", fontSize: 11, fontWeight: 700,
|
||||||
|
textTransform: "uppercase", letterSpacing: "0.2em",
|
||||||
|
color: "var(--color-text-muted)", background: "var(--color-surface-muted)",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Props ────────────────────────────────────────────────────────────────────
|
||||||
|
interface ArchivedUserTableProps {
|
||||||
|
users: ArchivedUser[];
|
||||||
|
loading: boolean;
|
||||||
|
search: string;
|
||||||
|
page: number;
|
||||||
|
pages: number;
|
||||||
|
onView: (user: ArchivedUser) => void;
|
||||||
|
onPageChange: (p: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── main table ───────────────────────────────────────────────────────────────
|
||||||
|
export function ArchivedUserTable({ users, loading, search, page, pages, onView, onPageChange }: ArchivedUserTableProps) {
|
||||||
|
return (
|
||||||
|
<div style={cardStyle}>
|
||||||
|
{/* column headers */}
|
||||||
|
<div dir="rtl" style={{ display: "grid", gridTemplateColumns: "2fr 1.5fr 1fr 1fr 1fr 100px", ...thStyle }}>
|
||||||
|
<span>الاسم</span>
|
||||||
|
<span>اسم المستخدم</span>
|
||||||
|
<span>الحالة</span>
|
||||||
|
<span style={{ textAlign: "center" }}>تاريخ الإنشاء</span>
|
||||||
|
<span style={{ textAlign: "center" }}>آخر تحديث</span>
|
||||||
|
<span style={{ textAlign: "center" }}>إجراءات</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* loading state */}
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "4rem 0", color: "var(--color-text-muted)" }}>
|
||||||
|
<Spinner size="sm" className="text-blue-600" />
|
||||||
|
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
) : users.length === 0 ? (
|
||||||
|
/* empty state */
|
||||||
|
<div style={{ textAlign: "center", padding: "4rem 1rem" }}>
|
||||||
|
<p style={{ fontSize: 13, color: "var(--color-text-muted)" }}>
|
||||||
|
{search ? `لا توجد نتائج لـ "${search}"` : "لا يوجد مستخدمون في الأرشيف."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* data rows */
|
||||||
|
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
||||||
|
{users.map((u, i) => (
|
||||||
|
<li key={u.id} style={{
|
||||||
|
display: "grid", gridTemplateColumns: "2fr 1.5fr 1fr 1fr 1fr 100px",
|
||||||
|
alignItems: "center", gap: "0.5rem", padding: "0.875rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent",
|
||||||
|
fontSize: 13,
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontWeight: 600, color: "var(--color-text-primary)", margin: 0 }}>{u.name}</p>
|
||||||
|
<p style={{ marginTop: 2, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--color-text-muted)" }}>{u.phone}</p>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>{u.userName ?? "—"}</span>
|
||||||
|
<StatusBadge active={u.isActive} />
|
||||||
|
<span style={{ textAlign: "center", fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||||
|
{new Date(u.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })}
|
||||||
|
</span>
|
||||||
|
<span style={{ textAlign: "center", fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||||
|
{new Date(u.updatedAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })}
|
||||||
|
</span>
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", gap: 4 }}>
|
||||||
|
{/* view only — delete/restore not implemented yet */}
|
||||||
|
<IconBtn title={`عرض ${u.name}`} color="#059669" bg="#ECFDF5" borderColor="#A7F3D0" onClick={() => onView(u)}>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
</IconBtn>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* pagination */}
|
||||||
|
{pages > 1 && (
|
||||||
|
<div dir="rtl" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", borderTop: "1px solid var(--color-border)", padding: "0.875rem 1.5rem" }}>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
|
||||||
|
صفحة <strong style={{ color: "var(--color-text-primary)" }}>{page}</strong> من <strong style={{ color: "var(--color-text-primary)" }}>{pages}</strong>
|
||||||
|
</span>
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
{[
|
||||||
|
{ label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 },
|
||||||
|
{ label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages },
|
||||||
|
].map(btn => (
|
||||||
|
<button key={btn.label} type="button" onClick={btn.action} disabled={btn.disabled}
|
||||||
|
style={{ borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-surface-muted)", padding: "0.375rem 0.875rem", fontSize: 12, color: "var(--color-text-secondary)", cursor: btn.disabled ? "not-allowed" : "pointer", opacity: btn.disabled ? 0.4 : 1, fontFamily: "var(--font-sans)" }}>
|
||||||
|
{btn.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
149
src/Components/car/archive/Archivedcardetailpanel.tsx
Normal file
149
src/Components/car/archive/Archivedcardetailpanel.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Spinner } from "../../UI";
|
||||||
|
import { STATUS_MAP, INS_MAP, fmtDate, isExpiringSoon } from "@/src/types/car";
|
||||||
|
import type { Car, InsuranceStatus } from "@/src/types/car";
|
||||||
|
|
||||||
|
interface ArchivedCarDetailPanelProps {
|
||||||
|
car: Car | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailRow({ label, value, mono = false, warn = false }: {
|
||||||
|
label: string; value: string; mono?: boolean; warn?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: "flex", justifyContent: "space-between", alignItems: "baseline",
|
||||||
|
padding: "0.6rem 0",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--color-text-muted)", fontWeight: 600 }}>{label}</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: mono ? "var(--font-mono)" : "var(--font-sans)",
|
||||||
|
color: warn ? "#D97706" : "var(--color-text-primary)",
|
||||||
|
fontWeight: warn ? 600 : 400,
|
||||||
|
}}>
|
||||||
|
{warn && value !== "—" ? "⚠ " : ""}{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArchivedCarDetailPanel({ car, loading, error, onClose }: ArchivedCarDetailPanelProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
const h = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
||||||
|
window.addEventListener("keydown", h);
|
||||||
|
return () => window.removeEventListener("keydown", h);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="archived-car-detail-title"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
style={{
|
||||||
|
position: "fixed", inset: 0, zIndex: 80,
|
||||||
|
background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "100%", maxWidth: 480,
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
borderRadius: "var(--radius-2xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
boxShadow: "0 24px 64px rgba(0,0,0,.18)",
|
||||||
|
overflow: "hidden",
|
||||||
|
display: "flex", flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* header */}
|
||||||
|
<div style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface-muted)",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>
|
||||||
|
مركبة مؤرشفة
|
||||||
|
</p>
|
||||||
|
<h2 id="archived-car-detail-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||||
|
{car ? `${car.manufacturer} ${car.model}` : "عرض المركبة"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} aria-label="إغلاق"
|
||||||
|
style={{ width: 34, height: 34, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", cursor: "pointer", fontSize: 18, color: "var(--color-text-muted)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* body */}
|
||||||
|
<div style={{ padding: "1.5rem", overflowY: "auto", maxHeight: "70vh" }} dir="rtl">
|
||||||
|
{loading && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 10, padding: "3rem 0", color: "var(--color-text-muted)" }}>
|
||||||
|
<Spinner size="sm" className="text-blue-600" />
|
||||||
|
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && error && (
|
||||||
|
<div style={{ padding: "1rem 1.25rem", borderRadius: "var(--radius-lg)", background: "#FEF2F2", border: "1px solid #FECACA", fontSize: 13, color: "#991B1B", fontWeight: 500, textAlign: "center" }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && car && (
|
||||||
|
<>
|
||||||
|
{/* status badges */}
|
||||||
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: "1.25rem" }}>
|
||||||
|
{(() => {
|
||||||
|
const s = STATUS_MAP[car.currentStatus];
|
||||||
|
return (
|
||||||
|
<span style={{ borderRadius: "var(--radius-full)", border: `1px solid ${s.border}`, background: s.bg, padding: "0.3rem 0.875rem", fontSize: 12, fontWeight: 700, color: s.color }}>
|
||||||
|
{s.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
{car.insuranceStatus && (() => {
|
||||||
|
const ins = INS_MAP[car.insuranceStatus as InsuranceStatus];
|
||||||
|
return (
|
||||||
|
<span style={{ borderRadius: "var(--radius-full)", border: `1px solid ${ins.color}33`, background: `${ins.color}11`, padding: "0.3rem 0.875rem", fontSize: 12, fontWeight: 700, color: ins.color }}>
|
||||||
|
تأمين: {ins.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
<span style={{ borderRadius: "var(--radius-full)", border: "1px solid #FECACA", background: "#FEF2F2", padding: "0.3rem 0.875rem", fontSize: 12, fontWeight: 700, color: "#DC2626" }}>
|
||||||
|
مؤرشفة
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DetailRow label="رقم اللوحة" value={`${car.plateLetters} ${car.plateNumber}`} mono />
|
||||||
|
<DetailRow label="الفرع" value={car.branch?.name ?? "—"} />
|
||||||
|
<DetailRow label="رقم الاستمارة" value={car.registrationNumber ?? "—"} mono />
|
||||||
|
<DetailRow label="رقم الهيكل (VIN)" value={car.vinNumber ?? "—"} mono />
|
||||||
|
<DetailRow label="انتهاء الاستمارة" value={fmtDate(car.registrationExpiryDate)} warn={isExpiringSoon(car.registrationExpiryDate)} />
|
||||||
|
<DetailRow label="انتهاء التأمين" value={fmtDate(car.insuranceExpiryDate)} warn={isExpiringSoon(car.insuranceExpiryDate)} />
|
||||||
|
<DetailRow label="تاريخ الإضافة" value={fmtDate(car.createdAt)} />
|
||||||
|
<DetailRow label="آخر تحديث" value={fmtDate(car.updatedAt)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* footer */}
|
||||||
|
<div style={{ padding: "1rem 1.5rem", borderTop: "1px solid var(--color-border)", background: "var(--color-surface-muted)", display: "flex", justifyContent: "flex-end" }}>
|
||||||
|
<button type="button" onClick={onClose}
|
||||||
|
style={{ height: 40, padding: "0 1.5rem", borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)", cursor: "pointer", fontFamily: "var(--font-sans)" }}>
|
||||||
|
إغلاق
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
src/Components/car/archive/Archivedcarsmodal.tsx
Normal file
120
src/Components/car/archive/Archivedcarsmodal.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Alert } from "../../UI";
|
||||||
|
import { ArchivedCarTable } from "./Archivedcartable";
|
||||||
|
import { ArchivedCarDetailPanel } from "./Archivedcardetailpanel";
|
||||||
|
import { useArchivedCars } from "@/src/hooks/archive/Usearchivedcars";
|
||||||
|
import type { Car } from "@/src/types/car";
|
||||||
|
|
||||||
|
interface ArchivedCarsModalProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ArchivedCarsModal({ onClose }: ArchivedCarsModalProps) {
|
||||||
|
const [viewCar, setViewCar] = useState<Car | null>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
cars, loading, total, pages, error,
|
||||||
|
page, search,
|
||||||
|
setPage, handleSearch, setError,
|
||||||
|
} = useArchivedCars();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog" aria-modal="true" aria-labelledby="archive-cars-modal-title"
|
||||||
|
onClick={e => { if (e.target === e.currentTarget) onClose(); }}
|
||||||
|
style={{
|
||||||
|
position: "fixed", inset: 0, zIndex: 70,
|
||||||
|
background: "rgba(15,23,42,0.6)", backdropFilter: "blur(4px)",
|
||||||
|
display: "flex", alignItems: "flex-start", justifyContent: "center",
|
||||||
|
padding: "2rem 1rem", overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{viewCar && (
|
||||||
|
<ArchivedCarDetailPanel
|
||||||
|
car={viewCar}
|
||||||
|
loading={false}
|
||||||
|
error={null}
|
||||||
|
onClose={() => setViewCar(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "100%", maxWidth: 980,
|
||||||
|
background: "var(--color-surface-sunken)",
|
||||||
|
borderRadius: "var(--radius-xl)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
boxShadow: "0 24px 64px rgba(0,0,0,.2)",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* header */}
|
||||||
|
<div dir="rtl" style={{
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||||
|
padding: "1.25rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#EA580C", fontWeight: 600, margin: 0 }}>
|
||||||
|
الأرشيف
|
||||||
|
</p>
|
||||||
|
<h2 id="archive-cars-modal-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||||
|
المركبات المؤرشفة
|
||||||
|
<span style={{ marginInlineStart: 8, fontSize: 13, fontWeight: 500, color: "var(--color-text-muted)" }}>
|
||||||
|
({total})
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} aria-label="إغلاق"
|
||||||
|
style={{ width: 34, height: 34, borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", cursor: "pointer", fontSize: 18, color: "var(--color-text-muted)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* body */}
|
||||||
|
<div style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}>
|
||||||
|
{/* search */}
|
||||||
|
<div dir="rtl" style={{ position: "relative", maxWidth: 320 }}>
|
||||||
|
<svg style={{ position: "absolute", right: 12, top: "50%", transform: "translateY(-50%)", width: 16, height: 16, color: "var(--color-text-hint)", pointerEvents: "none" }}
|
||||||
|
fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||||
|
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="بحث بالماركة أو اللوحة..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => handleSearch(e.target.value)}
|
||||||
|
dir="rtl"
|
||||||
|
style={{
|
||||||
|
width: "100%", height: 40,
|
||||||
|
paddingRight: 36, paddingLeft: 12,
|
||||||
|
borderRadius: "var(--radius-lg)",
|
||||||
|
border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)",
|
||||||
|
fontSize: 13, outline: "none",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
color: "var(--color-text-primary)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <Alert type="error" message={error} onClose={() => setError(null)} />}
|
||||||
|
|
||||||
|
<ArchivedCarTable
|
||||||
|
cars={cars}
|
||||||
|
loading={loading}
|
||||||
|
search={search}
|
||||||
|
page={page}
|
||||||
|
pages={pages}
|
||||||
|
onView={car => setViewCar(car)}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
src/Components/car/archive/Archivedcartable.tsx
Normal file
133
src/Components/car/archive/Archivedcartable.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Spinner } from "../../UI";
|
||||||
|
import { STATUS_MAP, fmtDateShort } from "@/src/types/car";
|
||||||
|
import type { Car } from "@/src/types/car";
|
||||||
|
|
||||||
|
// ── card / header styles ─────────────────────────────────────────────────────
|
||||||
|
const cardStyle: React.CSSProperties = {
|
||||||
|
borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)",
|
||||||
|
background: "var(--color-surface)", overflow: "hidden", boxShadow: "var(--shadow-card)",
|
||||||
|
};
|
||||||
|
const thStyle: React.CSSProperties = {
|
||||||
|
padding: "0.75rem 1.5rem", fontSize: 11, fontWeight: 700,
|
||||||
|
textTransform: "uppercase", letterSpacing: "0.2em",
|
||||||
|
color: "var(--color-text-muted)", background: "var(--color-surface-muted)",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── icon button ──────────────────────────────────────────────────────────────
|
||||||
|
function IconBtn({ onClick, title, color, bg, borderColor, children }: { onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<button type="button" title={title} aria-label={title} onClick={e => { e.stopPropagation(); onClick(); }}
|
||||||
|
style={{ width: 32, height: 32, borderRadius: "var(--radius-md)", border: `1px solid ${borderColor}`, background: bg, color, cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center", transition: "opacity 150ms" }}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Props ────────────────────────────────────────────────────────────────────
|
||||||
|
interface ArchivedCarTableProps {
|
||||||
|
cars: Car[];
|
||||||
|
loading: boolean;
|
||||||
|
search: string;
|
||||||
|
page: number;
|
||||||
|
pages: number;
|
||||||
|
onView: (car: Car) => void;
|
||||||
|
onPageChange: (p: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── main table ───────────────────────────────────────────────────────────────
|
||||||
|
export function ArchivedCarTable({ cars, loading, search, page, pages, onView, onPageChange }: ArchivedCarTableProps) {
|
||||||
|
return (
|
||||||
|
<div style={cardStyle}>
|
||||||
|
{/* column headers */}
|
||||||
|
<div dir="rtl" style={{ display: "grid", gridTemplateColumns: "2fr 1.5fr 1fr 1fr 1fr 100px", ...thStyle }}>
|
||||||
|
<span>المركبة</span>
|
||||||
|
<span>رقم اللوحة</span>
|
||||||
|
<span>الفرع</span>
|
||||||
|
<span>آخر حالة</span>
|
||||||
|
<span style={{ textAlign: "center" }}>تاريخ الإضافة</span>
|
||||||
|
<span style={{ textAlign: "center" }}>إجراءات</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* loading state */}
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "4rem 0", color: "var(--color-text-muted)" }}>
|
||||||
|
<Spinner size="sm" className="text-blue-600" />
|
||||||
|
<span style={{ fontSize: 13 }}>جارٍ التحميل…</span>
|
||||||
|
</div>
|
||||||
|
) : cars.length === 0 ? (
|
||||||
|
/* empty state */
|
||||||
|
<div style={{ textAlign: "center", padding: "4rem 1rem" }}>
|
||||||
|
<p style={{ fontSize: 13, color: "var(--color-text-muted)" }}>
|
||||||
|
{search ? `لا توجد نتائج لـ "${search}"` : "لا توجد مركبات في الأرشيف."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* data rows */
|
||||||
|
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
|
||||||
|
{cars.map((c, i) => {
|
||||||
|
const status = STATUS_MAP[c.currentStatus];
|
||||||
|
return (
|
||||||
|
<li key={c.id} style={{
|
||||||
|
display: "grid", gridTemplateColumns: "2fr 1.5fr 1fr 1fr 1fr 100px",
|
||||||
|
alignItems: "center", gap: "0.5rem", padding: "0.875rem 1.5rem",
|
||||||
|
borderBottom: "1px solid var(--color-border)",
|
||||||
|
background: i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent",
|
||||||
|
fontSize: 13,
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<p style={{ fontWeight: 600, color: "var(--color-text-primary)", margin: 0 }}>
|
||||||
|
{c.manufacturer} {c.model}
|
||||||
|
</p>
|
||||||
|
<p style={{ marginTop: 2, fontSize: 11, color: "var(--color-text-muted)" }}>{c.year}{c.color ? ` · ${c.color}` : ""}</p>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "#2563EB", fontWeight: 600 }}>
|
||||||
|
{c.plateLetters} {c.plateNumber}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: "var(--color-text-secondary)" }}>{c.branch?.name ?? "—"}</span>
|
||||||
|
<span style={{ display: "inline-flex", alignItems: "center", gap: 5, borderRadius: "var(--radius-full)", border: `1px solid ${status.border}`, background: status.bg, padding: "0.2rem 0.625rem", fontSize: 11, fontWeight: 700, color: status.color, width: "fit-content" }}>
|
||||||
|
<span style={{ width: 6, height: 6, borderRadius: "50%", background: status.dot }} />
|
||||||
|
{status.label}
|
||||||
|
</span>
|
||||||
|
<span style={{ textAlign: "center", fontSize: 11, color: "var(--color-text-muted)" }}>
|
||||||
|
{fmtDateShort(c.createdAt)}
|
||||||
|
</span>
|
||||||
|
<div style={{ display: "flex", justifyContent: "center", gap: 4 }}>
|
||||||
|
{/* view only — delete/restore not implemented yet */}
|
||||||
|
<IconBtn title={`عرض ${c.manufacturer} ${c.model}`} color="#059669" bg="#ECFDF5" borderColor="#A7F3D0" onClick={() => onView(c)}>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
</IconBtn>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* pagination */}
|
||||||
|
{pages > 1 && (
|
||||||
|
<div dir="rtl" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", borderTop: "1px solid var(--color-border)", padding: "0.875rem 1.5rem" }}>
|
||||||
|
<span style={{ fontSize: 12, color: "var(--color-text-muted)" }}>
|
||||||
|
صفحة <strong style={{ color: "var(--color-text-primary)" }}>{page}</strong> من <strong style={{ color: "var(--color-text-primary)" }}>{pages}</strong>
|
||||||
|
</span>
|
||||||
|
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||||
|
{[
|
||||||
|
{ label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 },
|
||||||
|
{ label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages },
|
||||||
|
].map(btn => (
|
||||||
|
<button key={btn.label} type="button" onClick={btn.action} disabled={btn.disabled}
|
||||||
|
style={{ borderRadius: "var(--radius-lg)", border: "1px solid var(--color-border)", background: "var(--color-surface-muted)", padding: "0.375rem 0.875rem", fontSize: 12, color: "var(--color-text-secondary)", cursor: btn.disabled ? "not-allowed" : "pointer", opacity: btn.disabled ? 0.4 : 1, fontFamily: "var(--font-sans)" }}>
|
||||||
|
{btn.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
65
src/hooks/archive/Usearchivedcars.ts
Normal file
65
src/hooks/archive/Usearchivedcars.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { getStoredToken } from "@/src/lib/auth";
|
||||||
|
import { carService } from "@/src/services/car.service";
|
||||||
|
import type { Car } from "@/src/types/car";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 12;
|
||||||
|
|
||||||
|
// ── useArchivedCars ───────────────────────────────────────────────────────────
|
||||||
|
// GET /cars/archived returns the full archived list at once (no page/search
|
||||||
|
// params on the backend), so pagination + search are done client-side here —
|
||||||
|
// mirrors the page/search/pages contract of useCars for a consistent UI.
|
||||||
|
|
||||||
|
export function useArchivedCars() {
|
||||||
|
const [allCars, setAllCars] = useState<Car[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const load = () => {
|
||||||
|
const token = getStoredToken();
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
carService
|
||||||
|
.getArchived(token)
|
||||||
|
.then((res) => {
|
||||||
|
const payload = (res as unknown as { data: { data: Car[] } }).data ?? res;
|
||||||
|
setAllCars((payload as { data: Car[] }).data ?? []);
|
||||||
|
})
|
||||||
|
.catch((err: Error) => setError(err.message))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (!search.trim()) return allCars;
|
||||||
|
const q = search.trim().toLowerCase();
|
||||||
|
return allCars.filter(c =>
|
||||||
|
c.manufacturer.toLowerCase().includes(q) ||
|
||||||
|
c.model.toLowerCase().includes(q) ||
|
||||||
|
c.plateNumber.toLowerCase().includes(q) ||
|
||||||
|
c.plateLetters.toLowerCase().includes(q),
|
||||||
|
);
|
||||||
|
}, [allCars, search]);
|
||||||
|
|
||||||
|
const total = filtered.length;
|
||||||
|
const pages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||||
|
const cars = useMemo(
|
||||||
|
() => filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
||||||
|
[filtered, page],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
cars, loading, error, total, pages, page, search,
|
||||||
|
setPage, handleSearch, refresh: load, setError,
|
||||||
|
};
|
||||||
|
}
|
||||||
49
src/hooks/archive/useArchivedDrivers.ts
Normal file
49
src/hooks/archive/useArchivedDrivers.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { getStoredToken } from "@/src/lib/auth";
|
||||||
|
import { archivedDriverService } from "@/src/services/archive/archivedDriver.service";
|
||||||
|
import type { ArchivedDriver } from "@/src/types/driver";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and paginates the archived drivers list, mirroring useArchivedUsers.
|
||||||
|
* Re-fetches whenever `page` or `search` changes.
|
||||||
|
*/
|
||||||
|
export function useArchivedDrivers() {
|
||||||
|
const [drivers, setDrivers] = useState<ArchivedDriver[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [pages, setPages] = useState(1);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
/** Fetches the current page/search slice of archived drivers from the API. */
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const token = getStoredToken();
|
||||||
|
const res = await archivedDriverService.getAll(page, search, token);
|
||||||
|
setDrivers(res.data.data);
|
||||||
|
setTotal(res.data.meta.total);
|
||||||
|
setPages(res.data.meta.totalPages);
|
||||||
|
setError(null);
|
||||||
|
} catch {
|
||||||
|
setError("تعذّر تحميل قائمة السائقين المؤرشفين. يرجى المحاولة لاحقاً.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [page, search]);
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
|
/** Updates the search term and resets to page 1. */
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
drivers, loading, total, pages, page, search, error,
|
||||||
|
setPage, handleSearch, clearError: () => setError(null),
|
||||||
|
refresh: load,
|
||||||
|
};
|
||||||
|
}
|
||||||
58
src/hooks/archive/useArchivedOrders.ts
Normal file
58
src/hooks/archive/useArchivedOrders.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { getStoredToken } from "@/src/lib/auth";
|
||||||
|
import { archivedOrderService } from "@/src/services/archive/archivedOrder.service";
|
||||||
|
import type { ArchivedOrder } from "@/src/types/order";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
export function useArchivedOrders() {
|
||||||
|
const [orders, setOrders] = useState<ArchivedOrder[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const token = getStoredToken();
|
||||||
|
const res = await archivedOrderService.getAll(token);
|
||||||
|
setOrders(res.data);
|
||||||
|
setError(null);
|
||||||
|
} catch {
|
||||||
|
setError("تعذّر تحميل الطلبات المؤرشفة. يرجى المحاولة لاحقاً.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
|
// Client-side search — endpoint has no `?search=` param in the sample payload
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (!search.trim()) return orders;
|
||||||
|
const q = search.trim().toLowerCase();
|
||||||
|
return orders.filter(
|
||||||
|
o =>
|
||||||
|
o.shipmentNumber.toLowerCase().includes(q) ||
|
||||||
|
o.recipientName.toLowerCase().includes(q) ||
|
||||||
|
o.recipientPhone.includes(q),
|
||||||
|
);
|
||||||
|
}, [orders, search]);
|
||||||
|
|
||||||
|
const pages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||||||
|
const paginated = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
orders: paginated,
|
||||||
|
total: filtered.length,
|
||||||
|
loading, pages, page, search, error,
|
||||||
|
setPage, handleSearch, clearError: () => setError(null),
|
||||||
|
refresh: load,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
src/hooks/archive/useArchivedTrips.ts
Normal file
45
src/hooks/archive/useArchivedTrips.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { getStoredToken } from "@/src/lib/auth";
|
||||||
|
import { archivedTripService } from "@/src/services/archive/archivedTrip.service";
|
||||||
|
import type { Trip } from "@/src/types/trip";
|
||||||
|
|
||||||
|
export function useArchivedTrips() {
|
||||||
|
const [trips, setTrips] = useState<Trip[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [pages, setPages] = useState(1);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// ── Load archived trips for the current page/search ───────────────────────
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const token = getStoredToken();
|
||||||
|
const res = await archivedTripService.getAll(page, search, token);
|
||||||
|
setTrips(res.data.data);
|
||||||
|
setTotal(res.data.meta.total);
|
||||||
|
setPages(res.data.meta.totalPages);
|
||||||
|
setError(null);
|
||||||
|
} catch {
|
||||||
|
// Network/API failure — surface a friendly Arabic message, matching useArchivedUsers
|
||||||
|
setError("تعذّر تحميل قائمة الرحلات المؤرشفة. يرجى المحاولة لاحقاً.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [page, search]);
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
trips, loading, total, pages, page, search, error,
|
||||||
|
setPage, handleSearch, clearError: () => setError(null),
|
||||||
|
refresh: load,
|
||||||
|
};
|
||||||
|
}
|
||||||
43
src/hooks/archive/useArchivedUsers.ts
Normal file
43
src/hooks/archive/useArchivedUsers.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { getStoredToken } from "@/src/lib/auth";
|
||||||
|
import { archivedUserService } from "@/src/services/archive/archivedUser.service";
|
||||||
|
import type { ArchivedUser } from "@/src/types/user";
|
||||||
|
|
||||||
|
export function useArchivedUsers() {
|
||||||
|
const [users, setUsers] = useState<ArchivedUser[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [pages, setPages] = useState(1);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const token = getStoredToken();
|
||||||
|
const res = await archivedUserService.getAll(page, search, token);
|
||||||
|
setUsers(res.data.data);
|
||||||
|
setTotal(res.data.meta.total);
|
||||||
|
setPages(res.data.meta.totalPages);
|
||||||
|
setError(null);
|
||||||
|
} catch {
|
||||||
|
setError("تعذّر تحميل قائمة الأرشيف. يرجى المحاولة لاحقاً.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [page, search]);
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
setPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
users, loading, total, pages, page, search, error,
|
||||||
|
setPage, handleSearch, clearError: () => setError(null),
|
||||||
|
refresh: load,
|
||||||
|
};
|
||||||
|
}
|
||||||
46
src/services/archive/archivedDriver.service.ts
Normal file
46
src/services/archive/archivedDriver.service.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { get } from "../api";
|
||||||
|
import type {
|
||||||
|
ArchivedDriverListResponse,
|
||||||
|
ArchivedDriverResponse,
|
||||||
|
ArchivedDriverStatusHistoryResponse,
|
||||||
|
} from "@/src/types/driver";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the query string for the archived drivers list endpoint.
|
||||||
|
* @param page - 1-indexed page number
|
||||||
|
* @param search - free-text search term (name or phone), omitted if empty
|
||||||
|
* @returns a query string starting with "?"
|
||||||
|
*/
|
||||||
|
function buildArchivedQuery(page: number, search: string): string {
|
||||||
|
return `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const archivedDriverService = {
|
||||||
|
/**
|
||||||
|
* Fetches the paginated list of archived drivers.
|
||||||
|
* @param page - page number to fetch
|
||||||
|
* @param search - optional search term
|
||||||
|
* @param token - auth token, or null if unauthenticated
|
||||||
|
*/
|
||||||
|
getAll: (page: number, search: string, token: string | null) =>
|
||||||
|
get<ArchivedDriverListResponse>(
|
||||||
|
`driver/archived${buildArchivedQuery(page, search)}`,
|
||||||
|
token,
|
||||||
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a single archived driver by id.
|
||||||
|
* @param id - archived driver id
|
||||||
|
* @param token - auth token, or null if unauthenticated
|
||||||
|
*/
|
||||||
|
getById: (id: string, token: string | null) =>
|
||||||
|
get<ArchivedDriverResponse>(`driver/archived/${id}`, token),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the status history for an archived driver.
|
||||||
|
* @param id - archived driver id
|
||||||
|
* @param token - auth token, or null if unauthenticated
|
||||||
|
*/
|
||||||
|
getStatusHistory: (id: string, token: string | null) =>
|
||||||
|
get<ArchivedDriverStatusHistoryResponse>(`driver/archived/driverStatus/${id}`, token),
|
||||||
|
};
|
||||||
8
src/services/archive/archivedOrder.service.ts
Normal file
8
src/services/archive/archivedOrder.service.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { get } from "../api";
|
||||||
|
import type { ArchivedOrderListResponse } from "@/src/types/order";
|
||||||
|
|
||||||
|
export const archivedOrderService = {
|
||||||
|
/** Fetching the full archived order list (endpoint returns no pagination meta) */
|
||||||
|
getAll: (token: string | null) =>
|
||||||
|
get<ArchivedOrderListResponse>("orders/archived", token),
|
||||||
|
};
|
||||||
20
src/services/archive/archivedTrip.service.ts
Normal file
20
src/services/archive/archivedTrip.service.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { get } from "../api";
|
||||||
|
import type { ArchivedTripListResponse, ArchivedTripResponse } from "@/src/types/trip";
|
||||||
|
|
||||||
|
/** Building a query string to fetch archived trips with pagination */
|
||||||
|
function buildArchivedQuery(page: number, search: string): string {
|
||||||
|
return `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const archivedTripService = {
|
||||||
|
/** Fetching the archived trip list, paginated */
|
||||||
|
getAll: (page: number, search: string, token: string | null) =>
|
||||||
|
get<ArchivedTripListResponse>(
|
||||||
|
`trip/archived${buildArchivedQuery(page, search)}`,
|
||||||
|
token,
|
||||||
|
),
|
||||||
|
|
||||||
|
/** Get a single archived trip by id */
|
||||||
|
getById: (id: string, token: string | null) =>
|
||||||
|
get<ArchivedTripResponse>(`trip/archived/${id}`, token),
|
||||||
|
};
|
||||||
25
src/services/archive/archivedUser.service.ts
Normal file
25
src/services/archive/archivedUser.service.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { get } from "../api";
|
||||||
|
import type {
|
||||||
|
ArchivedUserListResponse,
|
||||||
|
ArchivedUserResponse,
|
||||||
|
} from "@/src/types/user";
|
||||||
|
|
||||||
|
/** Building a query string to fetch archived users with pagination */
|
||||||
|
function buildArchivedQuery(page: number, search: string): string {
|
||||||
|
return `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const archivedUserService = {
|
||||||
|
/** Fetching the archived user list, paginated */
|
||||||
|
getAll: (page: number, search: string, token: string | null) =>
|
||||||
|
get<ArchivedUserListResponse>(
|
||||||
|
`users/archived${buildArchivedQuery(page, search)}`,
|
||||||
|
token,
|
||||||
|
),
|
||||||
|
|
||||||
|
/** Get a single archived user by id */
|
||||||
|
getById: (id: string, token: string | null) =>
|
||||||
|
get<ArchivedUserResponse>(`users/archived/${id}`, token),
|
||||||
|
|
||||||
|
// NOTE: delete/restore intentionally left out for now — see ticket follow-up.
|
||||||
|
};
|
||||||
@@ -126,3 +126,61 @@ export const DRIVER_CARD_TYPE_MAP: Record<DriverCardType, string> = {
|
|||||||
Restricted: "مقيدة",
|
Restricted: "مقيدة",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Archived Driver ──────────────────────────────────────────────────────────
|
||||||
|
// Resource returned by the /driver/archived endpoints. Kept as a distinct
|
||||||
|
// shape (rather than reusing Driver) since the archive endpoints return a
|
||||||
|
// flatter, slightly different set of fields — mirrors the ArchivedUser
|
||||||
|
// pattern in src/types/user.ts.
|
||||||
|
|
||||||
|
export interface ArchivedDriver {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
userName?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
phone: string;
|
||||||
|
address?: string | null;
|
||||||
|
nationality?: string | null;
|
||||||
|
nationalId?: string | null;
|
||||||
|
nationalIdType?: NationalIdType | null;
|
||||||
|
gosiNumber?: string | null;
|
||||||
|
licenseNumber?: string | null;
|
||||||
|
licenseType?: string | null;
|
||||||
|
licenseExpiry?: string | null;
|
||||||
|
photo?: string | null;
|
||||||
|
photoUrl?: string | null;
|
||||||
|
nationalPhoto?: string | null;
|
||||||
|
nationalPhotoUrl?: string | null;
|
||||||
|
driverCardPhoto?: string | null;
|
||||||
|
driverCardPhotoUrl?: string | null;
|
||||||
|
driverCardNumber?: string | null;
|
||||||
|
driverCardType?: DriverCardType | null;
|
||||||
|
status: DriverStatus;
|
||||||
|
isActive: boolean;
|
||||||
|
isDeleted: boolean;
|
||||||
|
driverType?: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /driver/archived
|
||||||
|
export interface ArchivedDriverListResponse {
|
||||||
|
data: {
|
||||||
|
data: ArchivedDriver[];
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /driver/archived/{id}
|
||||||
|
export interface ArchivedDriverResponse {
|
||||||
|
data: ArchivedDriver;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /driver/archived/driverStatus/{id}
|
||||||
|
export interface ArchivedDriverStatusHistoryResponse {
|
||||||
|
data: DriverStatusHistoryEntry[];
|
||||||
|
}
|
||||||
@@ -107,3 +107,33 @@ export interface OrdersResponse {
|
|||||||
totalPages: number;
|
totalPages: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Archived order resource returned by /orders/archived.
|
||||||
|
// NOTE: sample payload has no pagination meta — adjust if backend confirms otherwise.
|
||||||
|
export interface ArchivedOrder {
|
||||||
|
id: string;
|
||||||
|
shipmentNumber: string;
|
||||||
|
recipientName: string;
|
||||||
|
recipientPhone: string;
|
||||||
|
clientId: string;
|
||||||
|
tripId?: string | null;
|
||||||
|
pickupAddressId?: string | null;
|
||||||
|
deliveryAddressId?: string | null;
|
||||||
|
currentStatus: OrderStatus;
|
||||||
|
quantity: number;
|
||||||
|
subTotal?: number | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
statusHistory?: Array<{
|
||||||
|
id: string;
|
||||||
|
status: OrderStatus;
|
||||||
|
reason?: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArchivedOrderListResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
responseAt: string;
|
||||||
|
data: ArchivedOrder[];
|
||||||
|
}
|
||||||
@@ -155,3 +155,21 @@ export const TRIP_STATUS_MAP: Record<
|
|||||||
Cancelled: { label: "ملغاة", color: "#991B1B", bg: "#FEF2F2", border: "#FECACA", dot: "#DC2626" },
|
Cancelled: { label: "ملغاة", color: "#991B1B", bg: "#FEF2F2", border: "#FECACA", dot: "#DC2626" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Archived Trip types (mirrors ArchivedUser pattern in types/user.ts) ─────
|
||||||
|
// GET /trip/archived/{id}
|
||||||
|
export interface ArchivedTripResponse {
|
||||||
|
data: Trip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /trip/archived — paginated list
|
||||||
|
export interface ArchivedTripListResponse {
|
||||||
|
data: {
|
||||||
|
data: Trip[];
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -66,3 +66,49 @@ export type TableAction =
|
|||||||
role: { id?: string; name: string; description?: string };
|
role: { id?: string; name: string; description?: string };
|
||||||
branch: { id?: string; name: string };
|
branch: { id?: string; name: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Archived user resource returned by the archive endpoints
|
||||||
|
export interface ArchivedUser {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
userName?: string;
|
||||||
|
email?: string;
|
||||||
|
phone: string;
|
||||||
|
photo: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
isDeleted: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /v1/users/archived/{id}
|
||||||
|
export interface ArchivedUserResponse {
|
||||||
|
data: ArchivedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /v1/users/archived (note: this endpoint uses `meta`, not `pagination`,
|
||||||
|
// unlike the live users list — kept as a distinct shape rather than reusing
|
||||||
|
// ApiListResponse<T> from user.ts)
|
||||||
|
export interface ArchivedUserListResponse {
|
||||||
|
data: {
|
||||||
|
data: ArchivedUser[];
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared API error shape (validation_failed etc.)
|
||||||
|
export interface ApiErrorResponse {
|
||||||
|
success: false;
|
||||||
|
message: string;
|
||||||
|
responseAt: string;
|
||||||
|
error: {
|
||||||
|
code: string;
|
||||||
|
path: string;
|
||||||
|
details: { field: string; code: string }[];
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user