From b06824e0478124b91a35534edd4b9db0d5f97623 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Fri, 27 Mar 2026 13:55:59 +0100 Subject: [PATCH 1/2] feat(dashboard): add Profile and Settings links to sidebar footer with active styling --- frontend/app/(dashboard)/layout.tsx | 62 +++++++ .../app/(dashboard)/shipments/[id]/page.tsx | 155 ++++++++++++++++++ frontend/components/ShipmentCard.tsx | 51 +++++- frontend/components/ShipmentTimeline.tsx | 23 +++ frontend/services/shipmentApi.ts | 1 + 5 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 frontend/app/(dashboard)/layout.tsx diff --git a/frontend/app/(dashboard)/layout.tsx b/frontend/app/(dashboard)/layout.tsx new file mode 100644 index 00000000..2775d64f --- /dev/null +++ b/frontend/app/(dashboard)/layout.tsx @@ -0,0 +1,62 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; +import { ReactNode } from "react"; + +interface DashboardLayoutProps { + children: ReactNode; +} + +export default function DashboardLayout({ children }: DashboardLayoutProps) { + const pathname = usePathname(); + + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
{children}
+
+ ); +} diff --git a/frontend/app/(dashboard)/shipments/[id]/page.tsx b/frontend/app/(dashboard)/shipments/[id]/page.tsx index e69de29b..39dfc5b4 100644 --- a/frontend/app/(dashboard)/shipments/[id]/page.tsx +++ b/frontend/app/(dashboard)/shipments/[id]/page.tsx @@ -0,0 +1,155 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import { shipmentApi } from '@/services/shipmentApi'; +import ShipmentTimeline from '@/components/ShipmentTimeline'; +import { toast } from 'react-hot-toast'; + +type UserRole = 'shipper' | 'carrier' | 'admin'; +type ShipmentStatus = + | 'PENDING' + | 'ACCEPTED' + | 'IN_TRANSIT' + | 'DELIVERED' + | 'DISPUTED' + | 'COMPLETED' + | 'CANCELLED'; + +interface Shipment { + id: string; + description: string; + weight: number; + volume: number; + price: number; + origin: string; + destination: string; + shipperName: string; + carrierName?: string; + status: ShipmentStatus; +} + +interface HistoryEvent { + id: string; + status: ShipmentStatus; + timestamp: string; +} + +export default function ShipmentDetailPage() { + const params = useParams(); + const id = params?.id as string; + + const [shipment, setShipment] = useState(null); + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState(false); + + const fetchData = async () => { + try { + setLoading(true); + const [shipmentRes, historyRes] = await Promise.all([ + shipmentApi.getById(id), + shipmentApi.getHistory(id), + ]); + setShipment(shipmentRes); + setHistory(historyRes); + } catch (err) { + toast.error('Failed to load shipment'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (id) fetchData(); + }, [id]); + + const handleAction = async (action: string) => { + try { + setActionLoading(true); + await shipmentApi.performAction(id, action); + toast.success(`Action "${action}" successful`); + await fetchData(); + } catch (err) { + toast.error(`Failed to perform action: ${action}`); + } finally { + setActionLoading(false); + } + }; + + const renderActions = () => { + const role: UserRole = 'carrier'; // TODO: derive from auth context + const { status, carrierName } = shipment!; + + const buttons: JSX.Element[] = []; + + if (status === 'PENDING' && role !== 'shipper') { + buttons.push(); + } + if (status === 'ACCEPTED' && role === 'carrier' && carrierName) { + buttons.push(); + } + if (status === 'IN_TRANSIT' && role === 'carrier' && carrierName) { + buttons.push(); + } + if (status === 'DELIVERED' && role === 'shipper') { + buttons.push(); + } + if (['PENDING', 'ACCEPTED'].includes(status) && ['shipper', 'carrier', 'admin'].includes(role)) { + buttons.push(); + } + if (['IN_TRANSIT', 'DELIVERED'].includes(status) && ['shipper', 'carrier'].includes(role)) { + buttons.push(); + } + if (status === 'DISPUTED' && role === 'admin') { + buttons.push( +
+ + +
+ ); + } + if (['COMPLETED', 'CANCELLED'].includes(status)) { + buttons.push(

No further actions

); + } + + return
{buttons}
; + }; + + // ...rest of your JSX + + return ( +
+ {/* Left: Details + Actions */} +
+
+

Cargo

+
    +
  • Description: {shipment.description}
  • +
  • Weight: {shipment.weight} kg
  • +
  • Volume: {shipment.volume} m³
  • +
  • Price: ${shipment.price}
  • +
+
+ +
+

Parties

+
    +
  • Shipper: {shipment.shipperName}
  • +
  • Carrier: {shipment.carrierName || '—'}
  • +
+
+ +
+

Actions

+ {renderActions()} +
+
+ + {/* Right: Timeline */} +
+ +
+
+ ); +} diff --git a/frontend/components/ShipmentCard.tsx b/frontend/components/ShipmentCard.tsx index 4061cc95..9c035c1b 100644 --- a/frontend/components/ShipmentCard.tsx +++ b/frontend/components/ShipmentCard.tsx @@ -4,22 +4,67 @@ interface Shipment { id: string; origin: string; destination: string; + description?: string; weight?: number; + volume?: number; price?: number; pickupDate?: string; + shipperName?: string; + carrierName?: string; + status?: string; // optional for detail page context } interface Props { shipment: Shipment; + onClick?: (id: string) => void; // optional handler for navigation } -const ShipmentCard: React.FC = ({ shipment }) => { +const ShipmentCard: React.FC = ({ shipment, onClick }) => { return ( -
+
onClick?.(shipment.id)} + > + {/* Header */}

{shipment.origin} → {shipment.destination}

+ + {/* Cargo Details */}
    + {shipment.description &&
  • Description: {shipment.description}
  • } {shipment.weight &&
  • Weight: {shipment.weight} kg
  • } + {shipment.volume &&
  • Volume: {shipment.volume} m³
  • } {shipment.price &&
  • Price: ${shipment.price}
  • } - {shipment.pickupDate &&
  • Pickup: {new Date(shipment.pickupDate).toLocaleDateString()}
  • } + {shipment.pickupDate && ( +
  • Pickup: {new Date(shipment.pickupDate).toLocaleDateString()}
  • + )} +
+ + {/* Parties */} + {(shipment.shipperName || shipment.carrierName) && ( +
+

Shipper: {shipment.shipperName || '—'}

+

Carrier: {shipment.carrierName || '—'}

+
+ )} + + {/* Status */} + {shipment.status && ( +

+ Status: {shipment.status} +

+ )} + + {/* Action */} + +
+ ); +}; + +export default ShipmentCard; diff --git a/frontend/components/ShipmentTimeline.tsx b/frontend/components/ShipmentTimeline.tsx index e69de29b..6e187dff 100644 --- a/frontend/components/ShipmentTimeline.tsx +++ b/frontend/components/ShipmentTimeline.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +interface HistoryEvent { + id: string; + status: string; + timestamp: string; +} + +export default function ShipmentTimeline({ history }: { history: HistoryEvent[] }) { + return ( +
+

Status Timeline

+
    + {history.map((event) => ( +
  • + {event.status} + {new Date(event.timestamp).toLocaleString()} +
  • + ))} +
+
+ ); +} diff --git a/frontend/services/shipmentApi.ts b/frontend/services/shipmentApi.ts index 82582441..b16cd44c 100644 --- a/frontend/services/shipmentApi.ts +++ b/frontend/services/shipmentApi.ts @@ -1,5 +1,6 @@ import axios from 'axios'; + interface MarketplaceResponse { data: Shipment[]; totalPages: number; From 72b3146e77f38c209ba8af6384c422c53078c9e6 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Fri, 27 Mar 2026 14:14:17 +0100 Subject: [PATCH 2/2] feat(admin): add stats overview page with user, shipment, and revenue cards --- frontend/app/(dashboard)/admin/page.tsx | 117 ++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 frontend/app/(dashboard)/admin/page.tsx diff --git a/frontend/app/(dashboard)/admin/page.tsx b/frontend/app/(dashboard)/admin/page.tsx new file mode 100644 index 00000000..191b51d1 --- /dev/null +++ b/frontend/app/(dashboard)/admin/page.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { adminApi } from "@/lib/api/admin.api"; +import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; +import { useUser } from "@/lib/hooks/useUser"; // assumes you have a user hook + +export default function AdminStatsPage() { + const router = useRouter(); + const { user, loading: userLoading } = useUser(); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + + // Redirect non-admins + useEffect(() => { + if (!userLoading && user?.role !== "admin") { + router.push("/dashboard"); + } + }, [user, userLoading, router]); + + // Fetch stats + useEffect(() => { + async function fetchStats() { + try { + const data = await adminApi.getStats(); + setStats(data); + } catch (err) { + console.error("Failed to load stats", err); + } finally { + setLoading(false); + } + } + fetchStats(); + }, []); + + if (loading) { + return ( +
+ {Array.from({ length: 10 }).map((_, i) => ( + + ))} +
+ ); + } + + return ( +
+

Admin Overview

+ + {/* User stats */} +
+ + + + + + +
+ + {/* Shipment stats */} +
+ + + + + 0} + /> + +
+ + {/* Revenue */} +
+ +
+ + {/* Quick navigation */} +
+ + +
+
+ ); +} + +function StatCard({ + title, + value, + destructive = false, +}: { + title: string; + value: any; + destructive?: boolean; +}) { + return ( + + + {title} + + +

{value}

+
+
+ ); +}