diff --git a/frontend/app/(dashboard)/layout.tsx b/frontend/app/(dashboard)/layout.tsx index 19f6b497..2775d64f 100644 --- a/frontend/app/(dashboard)/layout.tsx +++ b/frontend/app/(dashboard)/layout.tsx @@ -1,101 +1,62 @@ -'use client'; +"use client"; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { cn } from '../../lib/utils'; -import { useAuthStore } from '../../stores/auth.store'; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { cn } from "@/lib/utils"; +import { ReactNode } from "react"; -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' }, -]; +interface DashboardLayoutProps { + children: ReactNode; +} -export default function DashboardLayout({ children }: { children: React.ReactNode }) { +export default function DashboardLayout({ children }: DashboardLayoutProps) { const pathname = usePathname(); - const { user, logout } = useAuthStore(); - - const navItems = - user?.role === 'carrier' - ? CARRIER_NAV - : user?.role === 'admin' - ? ADMIN_NAV - : SHIPPER_NAV; return ( -
+
{/* Sidebar */} -
); } diff --git a/frontend/app/(dashboard)/shipments/[id]/page.tsx b/frontend/app/(dashboard)/shipments/[id]/page.tsx index ab601c1f..90f97631 100644 --- a/frontend/app/(dashboard)/shipments/[id]/page.tsx +++ b/frontend/app/(dashboard)/shipments/[id]/page.tsx @@ -1,5 +1,79 @@ 'use client'; +<<<<<<< HEAD +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}`); +======= import { useEffect, useState } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { toast } from 'sonner'; @@ -45,11 +119,86 @@ export default function ShipmentDetailPage() { await reload(); } catch { toast.error('Action failed. Please try again.'); +>>>>>>> main } finally { setActionLoading(false); } }; +<<<<<<< HEAD + 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 */} +
+ +======= if (loading) { return (
@@ -299,6 +448,7 @@ export default function ShipmentDetailPage() {
+>>>>>>> main
); diff --git a/frontend/components/ShipmentCard.tsx b/frontend/components/ShipmentCard.tsx new file mode 100644 index 00000000..9c035c1b --- /dev/null +++ b/frontend/components/ShipmentCard.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +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, 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()}
  • + )} +
+ + {/* 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 new file mode 100644 index 00000000..6e187dff --- /dev/null +++ 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 new file mode 100644 index 00000000..b16cd44c --- /dev/null +++ b/frontend/services/shipmentApi.ts @@ -0,0 +1,26 @@ +import axios from 'axios'; + + +interface MarketplaceResponse { + data: Shipment[]; + totalPages: number; + totalCount: number; +} + +interface Shipment { + id: string; + origin: string; + destination: string; + weight?: number; + price?: number; + pickupDate?: string; +} + +export const shipmentApi = { + async marketplace(params?: { origin?: string; destination?: string; page?: number }): Promise { + const response = await axios.get('/api/shipments/marketplace', { + params, + }); + return response.data; + }, +};