From b06824e0478124b91a35534edd4b9db0d5f97623 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Fri, 27 Mar 2026 13:55:59 +0100 Subject: [PATCH] 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;