Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 45 additions & 84 deletions frontend/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex min-h-screen bg-background">
<div className="flex h-screen">
{/* Sidebar */}
<aside className="w-64 border-r bg-card flex flex-col">
<div className="h-16 flex items-center gap-2 px-6 border-b">
<div className="h-7 w-7 rounded-lg bg-primary flex items-center justify-center">
<span className="text-primary-foreground font-bold text-xs">FF</span>
</div>
<span className="font-bold text-foreground">FreightFlow</span>
</div>

<nav className="flex-1 p-4 space-y-1">
{navItems.map((item) => {
const active =
item.href === '/dashboard'
? pathname === '/dashboard'
: pathname.startsWith(item.href);

return (
<Link
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors',
active
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-accent hover:text-foreground',
)}
>
{item.label}
</Link>
);
})}
<aside className="w-64 bg-gray-50 border-r flex flex-col justify-between">
{/* Main nav block */}
<nav className="flex-1 p-4">
{/* Existing navItems logic here */}
{/* Example placeholder */}
<Link
href="/dashboard"
className={cn(
"block px-3 py-2 rounded-md text-sm font-medium",
pathname === "/dashboard" && "bg-gray-200 text-gray-900"
)}
>
Dashboard
</Link>
{/* Other navItems... */}
</nav>

{/* User footer */}
<div className="border-t p-4">
{user && (
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-primary/20 flex items-center justify-center text-xs font-bold text-primary">
{user.firstName[0]}
{user.lastName[0]}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">
{user.firstName} {user.lastName}
</p>
<p className="text-xs text-muted-foreground capitalize">{user.role}</p>
</div>
</div>
)}
<button
onClick={logout}
className="mt-3 w-full text-left text-xs text-muted-foreground hover:text-foreground transition-colors px-1"
{/* User footer section */}
<div className="p-4 border-t">
<Link
href="/profile"
className={cn(
"block px-3 py-2 rounded-md text-sm font-medium",
pathname === "/profile" && "bg-gray-200 text-gray-900"
)}
>
Profile
</Link>
<Link
href="/settings"
className={cn(
"block px-3 py-2 rounded-md text-sm font-medium",
pathname === "/settings" && "bg-gray-200 text-gray-900"
)}
>
Sign out
</button>
Settings
</Link>
</div>
</aside>

{/* Main content */}
<main className="flex-1 overflow-auto">{children}</main>
<main className="flex-1 overflow-y-auto">{children}</main>
</div>
);
}
150 changes: 150 additions & 0 deletions frontend/app/(dashboard)/shipments/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Shipment | null>(null);
const [history, setHistory] = useState<HistoryEvent[]>([]);
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';
Expand Down Expand Up @@ -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(<button key="accept" onClick={() => handleAction('accept')}>Accept Job</button>);
}
if (status === 'ACCEPTED' && role === 'carrier' && carrierName) {
buttons.push(<button key="pickup" onClick={() => handleAction('pickup')}>Mark Picked Up</button>);
}
if (status === 'IN_TRANSIT' && role === 'carrier' && carrierName) {
buttons.push(<button key="deliver" onClick={() => handleAction('deliver')}>Mark Delivered</button>);
}
if (status === 'DELIVERED' && role === 'shipper') {
buttons.push(<button key="confirm" onClick={() => handleAction('confirm')}>Confirm Delivery</button>);
}
if (['PENDING', 'ACCEPTED'].includes(status) && ['shipper', 'carrier', 'admin'].includes(role)) {
buttons.push(<button key="cancel" onClick={() => handleAction('cancel')}>Cancel</button>);
}
if (['IN_TRANSIT', 'DELIVERED'].includes(status) && ['shipper', 'carrier'].includes(role)) {
buttons.push(<button key="dispute" onClick={() => handleAction('dispute')}>Raise Dispute</button>);
}
if (status === 'DISPUTED' && role === 'admin') {
buttons.push(
<div key="resolve">
<button onClick={() => handleAction('resolve_complete')}>Resolve: Complete</button>
<button onClick={() => handleAction('resolve_cancel')}>Resolve: Cancel</button>
</div>
);
}
if (['COMPLETED', 'CANCELLED'].includes(status)) {
buttons.push(<p key="none">No further actions</p>);
}

return <div className="space-y-2">{buttons}</div>;
};

// ...rest of your JSX

return (
<div className="p-6 grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Details + Actions */}
<div className="lg:col-span-2 space-y-6">
<div className="border rounded p-4 bg-white">
<h2 className="text-lg font-semibold mb-2">Cargo</h2>
<ul className="text-sm text-gray-600 space-y-1">
<li>Description: {shipment.description}</li>
<li>Weight: {shipment.weight} kg</li>
<li>Volume: {shipment.volume} m³</li>
<li>Price: ${shipment.price}</li>
</ul>
</div>

<div className="border rounded p-4 bg-white">
<h2 className="text-lg font-semibold mb-2">Parties</h2>
<ul className="text-sm text-gray-600 space-y-1">
<li>Shipper: {shipment.shipperName}</li>
<li>Carrier: {shipment.carrierName || '—'}</li>
</ul>
</div>

<div className="border rounded p-4 bg-white">
<h2 className="text-lg font-semibold mb-2">Actions</h2>
{renderActions()}
</div>
</div>

{/* Right: Timeline */}
<div>
<ShipmentTimeline history={history} />
=======
if (loading) {
return (
<div className="p-6 max-w-4xl mx-auto space-y-4">
Expand Down Expand Up @@ -299,6 +448,7 @@ export default function ShipmentDetailPage() {
</CardContent>
</Card>
</div>
>>>>>>> main
</div>
</div>
);
Expand Down
70 changes: 70 additions & 0 deletions frontend/components/ShipmentCard.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ shipment, onClick }) => {
return (
<div
className="border rounded shadow-sm p-4 bg-white cursor-pointer hover:shadow-md transition"
onClick={() => onClick?.(shipment.id)}
>
{/* Header */}
<h2 className="text-lg font-semibold mb-2">
{shipment.origin} → {shipment.destination}
</h2>

{/* Cargo Details */}
<ul className="text-sm text-gray-600 space-y-1">
{shipment.description && <li>Description: {shipment.description}</li>}
{shipment.weight && <li>Weight: {shipment.weight} kg</li>}
{shipment.volume && <li>Volume: {shipment.volume} m³</li>}
{shipment.price && <li>Price: ${shipment.price}</li>}
{shipment.pickupDate && (
<li>Pickup: {new Date(shipment.pickupDate).toLocaleDateString()}</li>
)}
</ul>

{/* Parties */}
{(shipment.shipperName || shipment.carrierName) && (
<div className="mt-3 text-sm text-gray-700">
<p>Shipper: {shipment.shipperName || '—'}</p>
<p>Carrier: {shipment.carrierName || '—'}</p>
</div>
)}

{/* Status */}
{shipment.status && (
<p className="mt-2 text-xs font-medium text-gray-500">
Status: {shipment.status}
</p>
)}

{/* Action */}
<button
className="mt-3 px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"
onClick={() => onClick?.(shipment.id)}
>
View Details
</button>
</div>
);
};

export default ShipmentCard;
Loading
Loading