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;
+ },
+};