mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Project Initialization
This commit is contained in:
18
apps/edr-freight-web/Dockerfile.backoffice
Normal file
18
apps/edr-freight-web/Dockerfile.backoffice
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/edr-freight-web/backoffice/package.json ./apps/edr-freight-web/backoffice/
|
||||
COPY packages ./packages
|
||||
RUN pnpm install --frozen-lockfile --filter @edr/freight-backoffice...
|
||||
|
||||
FROM deps AS build
|
||||
COPY apps/edr-freight-web/backoffice ./apps/edr-freight-web/backoffice
|
||||
RUN pnpm --filter @edr/freight-backoffice build
|
||||
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
COPY --from=build /app/apps/edr-freight-web/backoffice/dist /usr/share/nginx/html
|
||||
EXPOSE 5183
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
18
apps/edr-freight-web/Dockerfile.portal
Normal file
18
apps/edr-freight-web/Dockerfile.portal
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/edr-freight-web/portal/package.json ./apps/edr-freight-web/portal/
|
||||
COPY packages ./packages
|
||||
RUN pnpm install --frozen-lockfile --filter @edr/freight-portal...
|
||||
|
||||
FROM deps AS build
|
||||
COPY apps/edr-freight-web/portal ./apps/edr-freight-web/portal
|
||||
RUN pnpm --filter @edr/freight-portal build
|
||||
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
COPY --from=build /app/apps/edr-freight-web/portal/dist /usr/share/nginx/html
|
||||
EXPOSE 5173
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
1
apps/edr-freight-web/backoffice/.env.example
Normal file
1
apps/edr-freight-web/backoffice/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_URL=http://localhost:3001
|
||||
12
apps/edr-freight-web/backoffice/index.html
Normal file
12
apps/edr-freight-web/backoffice/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EDR Freight Backoffice</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
39
apps/edr-freight-web/backoffice/package.json
Normal file
39
apps/edr-freight-web/backoffice/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@edr/freight-backoffice",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5183",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview --port 5183",
|
||||
"lint": "eslint src",
|
||||
"test": "vitest run",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edr/types": "workspace:*",
|
||||
"@edr/ui-common": "workspace:*",
|
||||
"@tanstack/react-query": "^5.59.0",
|
||||
"axios": "^1.7.7",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edr/eslint-config": "workspace:*",
|
||||
"@edr/tsconfig": "workspace:*",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"jsdom": "^25.0.1",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.8",
|
||||
"vitest": "^2.1.2"
|
||||
}
|
||||
}
|
||||
29
apps/edr-freight-web/backoffice/src/App.tsx
Normal file
29
apps/edr-freight-web/backoffice/src/App.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useNavigate, useLocation, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { DashboardLayout, type SidebarItem } from '@edr/ui-common';
|
||||
|
||||
import DashboardPage from './pages/dashboard/DashboardPage';
|
||||
|
||||
const sidebarItems: SidebarItem[] = [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
];
|
||||
|
||||
const App = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
title="EDR Freight Backoffice"
|
||||
sidebarItems={sidebarItems}
|
||||
activeHref={location.pathname}
|
||||
onNavigate={navigate}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
0
apps/edr-freight-web/backoffice/src/hooks/.gitkeep
Normal file
0
apps/edr-freight-web/backoffice/src/hooks/.gitkeep
Normal file
18
apps/edr-freight-web/backoffice/src/main.tsx
Normal file
18
apps/edr-freight-web/backoffice/src/main.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import App from './App';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
const DashboardPage = () => {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-semibold">EDR Freight Backoffice</h1>
|
||||
<p className="mt-2 text-gray-600">Backoffice — coming soon.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
0
apps/edr-freight-web/backoffice/src/store/.gitkeep
Normal file
0
apps/edr-freight-web/backoffice/src/store/.gitkeep
Normal file
0
apps/edr-freight-web/backoffice/src/types/.gitkeep
Normal file
0
apps/edr-freight-web/backoffice/src/types/.gitkeep
Normal file
0
apps/edr-freight-web/backoffice/src/utils/.gitkeep
Normal file
0
apps/edr-freight-web/backoffice/src/utils/.gitkeep
Normal file
1
apps/edr-freight-web/backoffice/src/vite-env.d.ts
vendored
Normal file
1
apps/edr-freight-web/backoffice/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
9
apps/edr-freight-web/backoffice/tsconfig.app.json
Normal file
9
apps/edr-freight-web/backoffice/tsconfig.app.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@edr/tsconfig/react.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"useDefineForClassFields": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
apps/edr-freight-web/backoffice/tsconfig.json
Normal file
7
apps/edr-freight-web/backoffice/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
14
apps/edr-freight-web/backoffice/tsconfig.node.json
Normal file
14
apps/edr-freight-web/backoffice/tsconfig.node.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@edr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
14
apps/edr-freight-web/backoffice/vite.config.ts
Normal file
14
apps/edr-freight-web/backoffice/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5183,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
1
apps/edr-freight-web/portal/.env.example
Normal file
1
apps/edr-freight-web/portal/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_URL=http://localhost:3001
|
||||
12
apps/edr-freight-web/portal/index.html
Normal file
12
apps/edr-freight-web/portal/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EDR Freight Portal</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
39
apps/edr-freight-web/portal/package.json
Normal file
39
apps/edr-freight-web/portal/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@edr/freight-portal",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5173",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview --port 5173",
|
||||
"lint": "eslint src",
|
||||
"test": "vitest run",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edr/types": "workspace:*",
|
||||
"@edr/ui-common": "workspace:*",
|
||||
"@tanstack/react-query": "^5.59.0",
|
||||
"axios": "^1.7.7",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edr/eslint-config": "workspace:*",
|
||||
"@edr/tsconfig": "workspace:*",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"jsdom": "^25.0.1",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.8",
|
||||
"vitest": "^2.1.2"
|
||||
}
|
||||
}
|
||||
50
apps/edr-freight-web/portal/src/App.tsx
Normal file
50
apps/edr-freight-web/portal/src/App.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useNavigate, useLocation, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { DashboardLayout, type SidebarItem } from '@edr/ui-common';
|
||||
|
||||
import BookingsPage from './pages/bookings/BookingsPage';
|
||||
import BookingDetailPage from './pages/bookings/BookingDetailPage';
|
||||
import CreateBookingPage from './pages/bookings/CreateBookingPage';
|
||||
import ConsignmentsPage from './pages/consignments/ConsignmentsPage';
|
||||
import ConsignmentDetailPage from './pages/consignments/ConsignmentDetailPage';
|
||||
import TrackingPage from './pages/tracking/TrackingPage';
|
||||
import BillingPage from './pages/billing/BillingPage';
|
||||
import TrainsPage from './pages/trains/TrainsPage';
|
||||
import DashboardPage from './pages/dashboard/DashboardPage';
|
||||
|
||||
const sidebarItems: SidebarItem[] = [
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Bookings', href: '/bookings' },
|
||||
{ label: 'Consignments', href: '/consignments' },
|
||||
{ label: 'Tracking', href: '/tracking' },
|
||||
{ label: 'Trains', href: '/trains' },
|
||||
{ label: 'Billing', href: '/billing' },
|
||||
];
|
||||
|
||||
const App = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
title="EDR Freight"
|
||||
sidebarItems={sidebarItems}
|
||||
activeHref={location.pathname}
|
||||
onNavigate={navigate}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/bookings" element={<BookingsPage />} />
|
||||
<Route path="/bookings/new" element={<CreateBookingPage />} />
|
||||
<Route path="/bookings/:id" element={<BookingDetailPage />} />
|
||||
<Route path="/consignments" element={<ConsignmentsPage />} />
|
||||
<Route path="/consignments/:id" element={<ConsignmentDetailPage />} />
|
||||
<Route path="/tracking" element={<TrackingPage />} />
|
||||
<Route path="/trains" element={<TrainsPage />} />
|
||||
<Route path="/billing" element={<BillingPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { FormEvent, useState } from 'react';
|
||||
import { Button, FormField } from '@edr/ui-common';
|
||||
|
||||
import type { CreateBookingPayload } from '../../services/bookings.service';
|
||||
|
||||
export interface BookingFormProps {
|
||||
onSubmit: (payload: CreateBookingPayload) => void;
|
||||
isSubmitting?: boolean;
|
||||
}
|
||||
|
||||
const BookingForm = ({ onSubmit, isSubmitting }: BookingFormProps) => {
|
||||
const [reference, setReference] = useState('');
|
||||
const [customerId, setCustomerId] = useState('');
|
||||
const [scheduledDate, setScheduledDate] = useState('');
|
||||
const [totalAmount, setTotalAmount] = useState('0');
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
onSubmit({
|
||||
reference,
|
||||
customerId,
|
||||
scheduledDate,
|
||||
totalAmount: Number(totalAmount),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
||||
<FormField label="Reference" value={reference} onChange={(e) => setReference(e.target.value)} required />
|
||||
<FormField label="Customer ID" value={customerId} onChange={(e) => setCustomerId(e.target.value)} required />
|
||||
<FormField
|
||||
label="Scheduled date"
|
||||
type="date"
|
||||
value={scheduledDate}
|
||||
onChange={(e) => setScheduledDate(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Total amount"
|
||||
type="number"
|
||||
value={totalAmount}
|
||||
onChange={(e) => setTotalAmount(e.target.value)}
|
||||
min="0"
|
||||
/>
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Create booking
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingForm;
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Freight } from '@edr/types';
|
||||
import { Table, type TableColumn } from '@edr/ui-common';
|
||||
|
||||
export interface BookingTableProps {
|
||||
bookings: Freight.IBooking[];
|
||||
}
|
||||
|
||||
const columns: TableColumn<Freight.IBooking>[] = [
|
||||
{ key: 'reference', header: 'Reference' },
|
||||
{ key: 'customerId', header: 'Customer' },
|
||||
{ key: 'status', header: 'Status' },
|
||||
{
|
||||
key: 'scheduledDate',
|
||||
header: 'Scheduled',
|
||||
render: (row) => new Date(row.scheduledDate).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
key: 'totalAmount',
|
||||
header: 'Total',
|
||||
render: (row) => row.totalAmount.toFixed(2),
|
||||
},
|
||||
];
|
||||
|
||||
const BookingTable = ({ bookings }: BookingTableProps) => (
|
||||
<Table columns={columns} data={bookings} rowKey={(row) => row.id} emptyMessage="No bookings yet" />
|
||||
);
|
||||
|
||||
export default BookingTable;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { FormEvent, useState } from 'react';
|
||||
import { Button, FormField } from '@edr/ui-common';
|
||||
|
||||
export interface ConsignmentFormProps {
|
||||
onSubmit: (payload: {
|
||||
bookingId: string;
|
||||
trackingNumber: string;
|
||||
cargoType: string;
|
||||
weightKg: number;
|
||||
originStation: string;
|
||||
destinationStation: string;
|
||||
}) => void;
|
||||
isSubmitting?: boolean;
|
||||
}
|
||||
|
||||
const ConsignmentForm = ({ onSubmit, isSubmitting }: ConsignmentFormProps) => {
|
||||
const [bookingId, setBookingId] = useState('');
|
||||
const [trackingNumber, setTrackingNumber] = useState('');
|
||||
const [cargoType, setCargoType] = useState('GENERAL');
|
||||
const [weightKg, setWeightKg] = useState('0');
|
||||
const [originStation, setOriginStation] = useState('');
|
||||
const [destinationStation, setDestinationStation] = useState('');
|
||||
|
||||
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
onSubmit({
|
||||
bookingId,
|
||||
trackingNumber,
|
||||
cargoType,
|
||||
weightKg: Number(weightKg),
|
||||
originStation,
|
||||
destinationStation,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
||||
<FormField label="Booking ID" value={bookingId} onChange={(e) => setBookingId(e.target.value)} required />
|
||||
<FormField
|
||||
label="Tracking #"
|
||||
value={trackingNumber}
|
||||
onChange={(e) => setTrackingNumber(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<FormField label="Cargo type" value={cargoType} onChange={(e) => setCargoType(e.target.value)} />
|
||||
<FormField
|
||||
label="Weight (kg)"
|
||||
type="number"
|
||||
value={weightKg}
|
||||
onChange={(e) => setWeightKg(e.target.value)}
|
||||
min="0"
|
||||
/>
|
||||
<FormField
|
||||
label="Origin station"
|
||||
value={originStation}
|
||||
onChange={(e) => setOriginStation(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Destination station"
|
||||
value={destinationStation}
|
||||
onChange={(e) => setDestinationStation(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Create consignment
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsignmentForm;
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Freight } from '@edr/types';
|
||||
import { Table, type TableColumn } from '@edr/ui-common';
|
||||
|
||||
export interface ConsignmentTableProps {
|
||||
consignments: Freight.IConsignment[];
|
||||
}
|
||||
|
||||
const columns: TableColumn<Freight.IConsignment>[] = [
|
||||
{ key: 'trackingNumber', header: 'Tracking #' },
|
||||
{ key: 'cargoType', header: 'Cargo' },
|
||||
{ key: 'status', header: 'Status' },
|
||||
{ key: 'originStation', header: 'Origin' },
|
||||
{ key: 'destinationStation', header: 'Destination' },
|
||||
{ key: 'weightKg', header: 'Weight (kg)', render: (row) => row.weightKg.toFixed(2) },
|
||||
];
|
||||
|
||||
const ConsignmentTable = ({ consignments }: ConsignmentTableProps) => (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={consignments}
|
||||
rowKey={(row) => row.id}
|
||||
emptyMessage="No consignments yet"
|
||||
/>
|
||||
);
|
||||
|
||||
export default ConsignmentTable;
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Freight } from '@edr/types';
|
||||
import { Badge } from '@edr/ui-common';
|
||||
|
||||
export interface TrackingTimelineProps {
|
||||
events: Freight.ITrackingEvent[];
|
||||
}
|
||||
|
||||
const TrackingTimeline = ({ events }: TrackingTimelineProps) => {
|
||||
if (events.length === 0) {
|
||||
return <div className="text-sm text-gray-500">No tracking events yet.</div>;
|
||||
}
|
||||
return (
|
||||
<ol className="relative ml-3 border-l border-gray-200">
|
||||
{events.map((event) => (
|
||||
<li key={event.id} className="mb-4 ml-4">
|
||||
<div className="absolute -left-1.5 mt-1.5 h-3 w-3 rounded-full bg-blue-500" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-gray-900">
|
||||
<span>{event.location}</span>
|
||||
<Badge tone="info">{event.status}</Badge>
|
||||
</div>
|
||||
<time className="text-xs text-gray-500">
|
||||
{new Date(event.occurredAt).toLocaleString()}
|
||||
</time>
|
||||
{event.description ? (
|
||||
<p className="text-sm text-gray-700">{event.description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrackingTimeline;
|
||||
16
apps/edr-freight-web/portal/src/hooks/useBookings.ts
Normal file
16
apps/edr-freight-web/portal/src/hooks/useBookings.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { bookingsService } from '../services/bookings.service';
|
||||
|
||||
export const useBookings = () =>
|
||||
useQuery({
|
||||
queryKey: ['bookings'],
|
||||
queryFn: bookingsService.list,
|
||||
});
|
||||
|
||||
export const useBooking = (id: string) =>
|
||||
useQuery({
|
||||
queryKey: ['bookings', id],
|
||||
queryFn: () => bookingsService.get(id),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
16
apps/edr-freight-web/portal/src/hooks/useConsignments.ts
Normal file
16
apps/edr-freight-web/portal/src/hooks/useConsignments.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { consignmentsService } from '../services/consignments.service';
|
||||
|
||||
export const useConsignments = () =>
|
||||
useQuery({
|
||||
queryKey: ['consignments'],
|
||||
queryFn: consignmentsService.list,
|
||||
});
|
||||
|
||||
export const useConsignment = (id: string) =>
|
||||
useQuery({
|
||||
queryKey: ['consignments', id],
|
||||
queryFn: () => consignmentsService.get(id),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
10
apps/edr-freight-web/portal/src/hooks/useTracking.ts
Normal file
10
apps/edr-freight-web/portal/src/hooks/useTracking.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { trackingService } from '../services/tracking.service';
|
||||
|
||||
export const useTracking = (consignmentId: string) =>
|
||||
useQuery({
|
||||
queryKey: ['tracking', consignmentId],
|
||||
queryFn: () => trackingService.forConsignment(consignmentId),
|
||||
enabled: Boolean(consignmentId),
|
||||
});
|
||||
18
apps/edr-freight-web/portal/src/main.tsx
Normal file
18
apps/edr-freight-web/portal/src/main.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import App from './App';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,11 @@
|
||||
const BillingPage = () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Billing</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Invoice list and payment status will live here. Wire up @tanstack/react-query to{' '}
|
||||
<code>/billing/invoices</code> when the feature is built out.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default BillingPage;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useBooking } from '../../hooks/useBookings';
|
||||
|
||||
const BookingDetailPage = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { data: booking, isLoading } = useBooking(id ?? '');
|
||||
|
||||
if (isLoading) return <div className="text-sm text-gray-500">Loading…</div>;
|
||||
if (!booking) return <div className="text-sm text-red-600">Booking not found.</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Booking {booking.reference}</h1>
|
||||
<dl className="grid grid-cols-2 gap-2 text-sm">
|
||||
<dt className="text-gray-500">Status</dt>
|
||||
<dd className="text-gray-900">{booking.status}</dd>
|
||||
<dt className="text-gray-500">Customer ID</dt>
|
||||
<dd className="text-gray-900">{booking.customerId}</dd>
|
||||
<dt className="text-gray-500">Scheduled</dt>
|
||||
<dd className="text-gray-900">{new Date(booking.scheduledDate).toLocaleString()}</dd>
|
||||
<dt className="text-gray-500">Total amount</dt>
|
||||
<dd className="text-gray-900">{booking.totalAmount.toFixed(2)}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingDetailPage;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@edr/ui-common';
|
||||
|
||||
import BookingTable from '../../components/bookings/BookingTable';
|
||||
import { useBookings } from '../../hooks/useBookings';
|
||||
|
||||
const BookingsPage = () => {
|
||||
const { data, isLoading } = useBookings();
|
||||
const items = data?.items ?? [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Bookings</h1>
|
||||
<Link to="/bookings/new">
|
||||
<Button>New booking</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{isLoading ? <div className="text-sm text-gray-500">Loading…</div> : <BookingTable bookings={items} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingsPage;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import BookingForm from '../../components/bookings/BookingForm';
|
||||
import { bookingsService } from '../../services/bookings.service';
|
||||
|
||||
const CreateBookingPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: bookingsService.create,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['bookings'] });
|
||||
navigate('/bookings');
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="max-w-lg">
|
||||
<h1 className="mb-4 text-2xl font-semibold text-gray-900">New booking</h1>
|
||||
<BookingForm onSubmit={mutation.mutate} isSubmitting={mutation.isPending} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateBookingPage;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useConsignment } from '../../hooks/useConsignments';
|
||||
|
||||
const ConsignmentDetailPage = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { data, isLoading } = useConsignment(id ?? '');
|
||||
|
||||
if (isLoading) return <div className="text-sm text-gray-500">Loading…</div>;
|
||||
if (!data) return <div className="text-sm text-red-600">Consignment not found.</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Consignment {data.trackingNumber}</h1>
|
||||
<dl className="grid grid-cols-2 gap-2 text-sm">
|
||||
<dt className="text-gray-500">Status</dt>
|
||||
<dd className="text-gray-900">{data.status}</dd>
|
||||
<dt className="text-gray-500">Cargo</dt>
|
||||
<dd className="text-gray-900">{data.cargoType}</dd>
|
||||
<dt className="text-gray-500">Origin</dt>
|
||||
<dd className="text-gray-900">{data.originStation}</dd>
|
||||
<dt className="text-gray-500">Destination</dt>
|
||||
<dd className="text-gray-900">{data.destinationStation}</dd>
|
||||
<dt className="text-gray-500">Weight</dt>
|
||||
<dd className="text-gray-900">{data.weightKg.toFixed(2)} kg</dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsignmentDetailPage;
|
||||
@@ -0,0 +1,20 @@
|
||||
import ConsignmentTable from '../../components/consignments/ConsignmentTable';
|
||||
import { useConsignments } from '../../hooks/useConsignments';
|
||||
|
||||
const ConsignmentsPage = () => {
|
||||
const { data, isLoading } = useConsignments();
|
||||
const items = data?.items ?? [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Consignments</h1>
|
||||
{isLoading ? (
|
||||
<div className="text-sm text-gray-500">Loading…</div>
|
||||
) : (
|
||||
<ConsignmentTable consignments={items} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsignmentsPage;
|
||||
@@ -0,0 +1,10 @@
|
||||
const DashboardPage = () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Freight Dashboard</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Operational KPIs (bookings, consignments, on-time rate, revenue) go here.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, FormField } from '@edr/ui-common';
|
||||
|
||||
import TrackingTimeline from '../../components/tracking/TrackingTimeline';
|
||||
import { useTracking } from '../../hooks/useTracking';
|
||||
|
||||
const TrackingPage = () => {
|
||||
const [consignmentId, setConsignmentId] = useState('');
|
||||
const [activeId, setActiveId] = useState('');
|
||||
const { data, isFetching } = useTracking(activeId);
|
||||
|
||||
return (
|
||||
<div className="flex max-w-2xl flex-col gap-4">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Tracking</h1>
|
||||
<div className="flex items-end gap-3">
|
||||
<FormField
|
||||
label="Consignment ID"
|
||||
value={consignmentId}
|
||||
onChange={(event) => setConsignmentId(event.target.value)}
|
||||
placeholder="UUID"
|
||||
/>
|
||||
<Button onClick={() => setActiveId(consignmentId)}>Look up</Button>
|
||||
</div>
|
||||
{isFetching ? (
|
||||
<div className="text-sm text-gray-500">Loading…</div>
|
||||
) : (
|
||||
<TrackingTimeline events={data ?? []} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrackingPage;
|
||||
10
apps/edr-freight-web/portal/src/pages/trains/TrainsPage.tsx
Normal file
10
apps/edr-freight-web/portal/src/pages/trains/TrainsPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
const TrainsPage = () => (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Trains</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Fleet roster, capacity, and maintenance status. Connect to <code>/trains</code> when ready.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TrainsPage;
|
||||
29
apps/edr-freight-web/portal/src/services/bookings.service.ts
Normal file
29
apps/edr-freight-web/portal/src/services/bookings.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Freight, PaginatedResponse } from '@edr/types';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
|
||||
export interface CreateBookingPayload {
|
||||
reference: string;
|
||||
customerId: string;
|
||||
scheduledDate: string;
|
||||
totalAmount: number;
|
||||
trainId?: string;
|
||||
}
|
||||
|
||||
export const bookingsService = {
|
||||
list: async (): Promise<PaginatedResponse<Freight.IBooking>> => {
|
||||
const { data } = await api.get('/bookings');
|
||||
return data.data;
|
||||
},
|
||||
get: async (id: string): Promise<Freight.IBooking> => {
|
||||
const { data } = await api.get(`/bookings/${id}`);
|
||||
return data.data;
|
||||
},
|
||||
create: async (payload: CreateBookingPayload): Promise<Freight.IBooking> => {
|
||||
const { data } = await api.post('/bookings', payload);
|
||||
return data.data;
|
||||
},
|
||||
remove: async (id: string): Promise<void> => {
|
||||
await api.delete(`/bookings/${id}`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Freight, PaginatedResponse } from '@edr/types';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
|
||||
export const consignmentsService = {
|
||||
list: async (): Promise<PaginatedResponse<Freight.IConsignment>> => {
|
||||
const { data } = await api.get('/consignments');
|
||||
return data.data;
|
||||
},
|
||||
get: async (id: string): Promise<Freight.IConsignment> => {
|
||||
const { data } = await api.get(`/consignments/${id}`);
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
10
apps/edr-freight-web/portal/src/services/tracking.service.ts
Normal file
10
apps/edr-freight-web/portal/src/services/tracking.service.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Freight } from '@edr/types';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
|
||||
export const trackingService = {
|
||||
forConsignment: async (consignmentId: string): Promise<Freight.ITrackingEvent[]> => {
|
||||
const { data } = await api.get(`/tracking/${consignmentId}`);
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
12
apps/edr-freight-web/portal/src/store/index.ts
Normal file
12
apps/edr-freight-web/portal/src/store/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface AppState {
|
||||
// TODO: integrate @edr/auth — currentUser will be sourced from the auth package
|
||||
isSidebarOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set) => ({
|
||||
isSidebarOpen: true,
|
||||
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
|
||||
}));
|
||||
6
apps/edr-freight-web/portal/src/types/index.ts
Normal file
6
apps/edr-freight-web/portal/src/types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type { Freight } from '@edr/types';
|
||||
|
||||
export interface NavItem {
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
9
apps/edr-freight-web/portal/src/utils/api.ts
Normal file
9
apps/edr-freight-web/portal/src/utils/api.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
});
|
||||
|
||||
// TODO: integrate @edr/auth — add a request interceptor here that attaches
|
||||
// the bearer token from the auth package and a response interceptor that
|
||||
// triggers a refresh on 401.
|
||||
9
apps/edr-freight-web/portal/src/vite-env.d.ts
vendored
Normal file
9
apps/edr-freight-web/portal/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
9
apps/edr-freight-web/portal/tsconfig.app.json
Normal file
9
apps/edr-freight-web/portal/tsconfig.app.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@edr/tsconfig/react.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"useDefineForClassFields": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
apps/edr-freight-web/portal/tsconfig.json
Normal file
7
apps/edr-freight-web/portal/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
14
apps/edr-freight-web/portal/tsconfig.node.json
Normal file
14
apps/edr-freight-web/portal/tsconfig.node.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@edr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
14
apps/edr-freight-web/portal/vite.config.ts
Normal file
14
apps/edr-freight-web/portal/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 5173,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user