diff --git a/frontend/app/(dashboard)/admin/shipments/page.tsx b/frontend/app/(dashboard)/admin/shipments/page.tsx new file mode 100644 index 00000000..28af77b7 --- /dev/null +++ b/frontend/app/(dashboard)/admin/shipments/page.tsx @@ -0,0 +1,216 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { useAuthStore } from '../../../../stores/auth.store'; +import { adminApi } from '../../../../lib/api/admin.api'; +import { Shipment } from '../../../../types/shipment.types'; +import { Button } from '../../../../components/ui/button'; +import { Card } from '../../../../components/ui/card'; +import { toast } from 'sonner'; + +const TABS = [ + 'All', + 'Pending', + 'Accepted', + 'In Transit', + 'Delivered', + 'Completed', + 'Disputed', + 'Cancelled', +]; + +export default function AdminShipmentsPage() { + const router = useRouter(); + const { user } = useAuthStore(); + + const [shipments, setShipments] = useState([]); + const [loading, setLoading] = useState(true); + + const [activeTab, setActiveTab] = useState('All'); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + const loadShipments = useCallback(async () => { + try { + setLoading(true); + const res = await adminApi.listShipments(page, activeTab); + setShipments(res.data || []); + setTotalPages(res.totalPages || Math.max(1, Math.ceil((res.total || 0) / (res.limit || 10)))); + } catch (err) { + toast.error((err as Error).message || 'Failed to load shipments'); + } finally { + setLoading(false); + } + }, [page, activeTab]); + + useEffect(() => { + if (user && user.role !== 'admin') { + router.push('/dashboard'); + } else if (user) { + loadShipments(); + } + }, [user, router, loadShipments]); + + if (!user || user.role !== 'admin') { + return null; + } + + const getStatusBadge = (status: string) => { + switch (status) { + case 'completed': + return 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400'; + case 'disputed': + case 'cancelled': + return 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400'; + case 'pending': + return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-400'; + case 'in_transit': + return 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-400'; + case 'delivered': + return 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-400'; + case 'accepted': + return 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-400'; + default: + return 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400'; + } + }; + + return ( +
+
+

Shipment Oversight

+

Monitor all platform shipments.

+
+ +
+ {TABS.map((tab) => ( + + ))} +
+ + + + + + + + + + + + + + + + + {loading && ( + + + + )} + {!loading && shipments.length === 0 && ( + + + + )} + {!loading && + shipments.map((shipment) => ( + + + + + + + + + + + ))} + +
Tracking #RouteShipperCarrierStatusPriceCreated DateActions
+
+ {[1, 2, 3].map((i) => ( +
+
+
+
+
+
+
+
+
+
+ ))} +
+
+ No shipments found. +
{shipment.trackingNumber || shipment.id.slice(0, 8).toUpperCase()} + {shipment.origin} + + {shipment.destination} + + {shipment.shipper ? `${shipment.shipper.firstName} ${shipment.shipper.lastName}` : 'N/A'} + + {shipment.carrier ? `${shipment.carrier.firstName} ${shipment.carrier.lastName}` : 'Unassigned'} + + + {shipment.status.replace('_', ' ')} + + + {shipment.price.toLocaleString()} {shipment.currency || 'USD'} + + {new Date(shipment.createdAt).toLocaleDateString()} + + +
+
+ + {totalPages > 1 && ( +
+

+ Page {page} of {totalPages} +

+
+ + +
+
+ )} +
+ ); +} diff --git a/frontend/app/(dashboard)/admin/users/page.tsx b/frontend/app/(dashboard)/admin/users/page.tsx new file mode 100644 index 00000000..82c6859c --- /dev/null +++ b/frontend/app/(dashboard)/admin/users/page.tsx @@ -0,0 +1,228 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuthStore } from '../../../../stores/auth.store'; +import { adminApi } from '../../../../lib/api/admin.api'; +import { User } from '../../../../types/auth.types'; +import { Button } from '../../../../components/ui/button'; +import { Card } from '../../../../components/ui/card'; +import { toast } from 'sonner'; + +export default function AdminUsersPage() { + const router = useRouter(); + const { user } = useAuthStore(); + + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + + const [roleFilter, setRoleFilter] = useState('All Roles'); + const [statusFilter, setStatusFilter] = useState('All'); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + + const loadUsers = useCallback(async () => { + try { + setLoading(true); + const res = await adminApi.listUsers(page, roleFilter, statusFilter); + setUsers(res.users || []); + setTotalPages(res.totalPages || Math.max(1, Math.ceil((res.total || 0) / (res.limit || 10)))); + } catch (err) { + toast.error((err as Error).message || 'Failed to load users'); + } finally { + setLoading(false); + } + }, [page, roleFilter, statusFilter]); + + useEffect(() => { + if (user && user.role !== 'admin') { + router.push('/dashboard'); + } else if (user) { + loadUsers(); + } + }, [user, router, loadUsers]); + + const handleRoleChange = async (targetUserId: string, newRole: string) => { + try { + await adminApi.changeUserRole(targetUserId, newRole); + toast.success('User role updated'); + loadUsers(); + } catch (err) { + toast.error((err as Error).message || 'Failed to change role'); + } + }; + + const handleStatusToggle = async (targetUser: User) => { + try { + if (targetUser.isActive) { + await adminApi.deactivateUser(targetUser.id); + toast.success('User deactivated'); + } else { + await adminApi.activateUser(targetUser.id); + toast.success('User activated'); + } + loadUsers(); + } catch (err) { + toast.error((err as Error).message || 'Failed to update status'); + } + }; + + if (!user || user.role !== 'admin') { + return null; // Don't render while redirecting or loading auth + } + + return ( +
+
+

Manage Users

+

View and manage all users on the platform.

+
+ + +
+ Role: + {['All Roles', 'Shippers', 'Carriers', 'Admins'].map((role) => ( + + ))} +
+
+ Status: + {['All', 'Active', 'Inactive'].map((status) => ( + + ))} +
+
+ + + + + + + + + + + + + + + {loading && users.length === 0 && ( + + + + )} + {!loading && users.length === 0 && ( + + + + )} + {users.map((u) => { + const isSelf = u.id === user.id; + + return ( + + + + + + + + + ); + })} + +
NameEmailRoleStatusJoined DateActions
+ Loading users... +
+ No users found. +
+ {u.firstName} {u.lastName} {isSelf && (you)} + {u.email} + {isSelf ? ( + {u.role} + ) : ( + + )} + + + {u.isActive ? 'Active' : 'Inactive'} + + + {new Date(u.createdAt).toLocaleDateString()} + + {!isSelf && ( + + )} +
+
+ + {totalPages > 1 && ( +
+

+ Page {page} of {totalPages} +

+
+ + +
+
+ )} +
+ ); +} diff --git a/frontend/app/(dashboard)/layout.tsx b/frontend/app/(dashboard)/layout.tsx index 2775d64f..6b8f386d 100644 --- a/frontend/app/(dashboard)/layout.tsx +++ b/frontend/app/(dashboard)/layout.tsx @@ -5,9 +5,26 @@ import { usePathname } from "next/navigation"; import { cn } from "@/lib/utils"; import { ReactNode } from "react"; -interface DashboardLayoutProps { - children: ReactNode; -} +const SHIPPER_NAV = [ + { href: '/dashboard', label: 'Dashboard' }, + { href: '/shipments', label: 'My Shipments' }, + { href: '/shipments/new', label: 'Create Shipment' }, +]; + +const CARRIER_NAV = [ + { href: '/dashboard', label: 'Dashboard' }, + { href: '/shipments', label: 'My Jobs' }, + { href: '/marketplace', label: 'Marketplace' }, +]; + +const ADMIN_NAV = [ + { href: '/dashboard', label: 'Dashboard' }, + { href: '/shipments', label: 'All Shipments' }, + { href: '/marketplace', label: 'Marketplace' }, + { href: '/admin', label: 'Admin Panel' }, + { href: '/admin/users', label: 'Manage Users' }, + { href: '/admin/shipments', label: 'Shipment Oversight' }, +]; export default function DashboardLayout({ children }: DashboardLayoutProps) { const pathname = usePathname(); @@ -15,21 +32,36 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) { return (
{/* Sidebar */} -