diff --git a/README.md b/README.md index 302dab618..fe91ea611 100644 --- a/README.md +++ b/README.md @@ -1 +1,259 @@ -# edr-platform +# EDR Platform + +Monorepo for the **Ethio-Djibouti Railway** digital platform. Hosts two product lines — **Freight Management** and **Passenger Management** — each with a NestJS API plus React portal and back-office web apps, sharing TypeScript types, NestJS utilities, and a React component library. + +--- + +## Tech Stack + +### Backend + +| Layer | Tech | +| ---------------- | ----------------------------------------------- | +| Runtime | Node.js ≥ 20 | +| Framework | NestJS 11 (modular architecture) | +| Language | TypeScript 5 (strict mode, project-wide) | +| ORM | TypeORM 0.3 (UUID PKs, soft deletes, `snake_case` columns) | +| Database | PostgreSQL 16 (one DB per domain) | +| Validation | class-validator + class-transformer | +| API docs | Swagger via `@nestjs/swagger` | +| Messaging | `@nestjs/microservices` (inter-service ready) | +| Testing | Jest + Supertest | + +### Frontend + +| Layer | Tech | +| ---------------- | ----------------------------------------------- | +| Framework | React 18 + Vite 5 | +| Language | TypeScript 5 (strict) | +| Routing | React Router v6 | +| Styling | Tailwind CSS v4 (`@tailwindcss/vite`) + `tailwind-merge` + `class-variance-authority` | +| UI primitives | Radix UI (meta `radix-ui` package, shadcn-style components) | +| Icons | lucide-react | +| State / Data | Zustand (client state) · TanStack Query (server state) | +| HTTP | Axios | +| Auth UI | `@tria-plc/iamui-common` (external IAM) | + +### Shared Packages + +| Package | Purpose | +| ---------------------- | -------------------------------------------------------------------- | +| `@edr/types` | Shared TypeScript interfaces and enums | +| `@edr/api-common` | NestJS decorators, filters, interceptors, pipes, `BaseEntity`, `BaseRepository` | +| `@edr/ui-common` | Shared React components (`DashboardLayout`, `Sidebar`, `Button`, `Modal`, etc.) and theme tokens | +| `@edr/eslint-config` | Shared ESLint configs (base / nestjs / react) | +| `@edr/tsconfig` | Shared TypeScript configs | +| `@edr/prettier-config` | Shared Prettier configuration | + +### Tooling + +- **pnpm 9** — workspace package manager (sole supported PM) +- **Turborepo 2** — task orchestrator with caching +- **Husky + lint-staged + commitlint** — pre-commit ESLint/Prettier and conventional-commit enforcement +- **Docker Compose** — local Postgres instances + production stack (see `infrastructure/`) +- **Nginx** — reverse proxy / static asset server in production + +### Planned Integrations + +- **MinIO** — S3-compatible object storage for documents (object keys already shaped under `edr-freight/{linkedType}/{ref}/{filename}`) + +--- + +## Architecture + +### High-level layout + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ EDR Platform (monorepo) │ +├──────────────────────┬───────────────────────────────────────────┤ +│ Freight domain │ Passenger domain │ +│ ┌────────────────┐ │ ┌────────────────┐ │ +│ │ freight-portal │ │ │ passenger- │ │ +│ │ (React) │ │ │ portal (React)│ │ +│ ├────────────────┤ │ ├────────────────┤ │ +│ │ freight- │ │ │ passenger- │ │ +│ │ backoffice │ │ │ backoffice │ │ +│ └───────┬────────┘ │ └───────┬────────┘ │ +│ │ │ │ │ +│ ▼ │ ▼ │ +│ ┌────────────────┐ │ ┌────────────────┐ │ +│ │ freight-api │ │ │ passenger-api │ │ +│ │ (NestJS) │ │ │ (NestJS) │ │ +│ └───────┬────────┘ │ └───────┬────────┘ │ +│ │ │ │ │ +│ ▼ │ ▼ │ +│ ┌────────────────┐ │ ┌────────────────┐ │ +│ │ postgres- │ │ │ postgres- │ │ +│ │ freight │ │ │ passenger │ │ +│ └────────────────┘ │ └────────────────┘ │ +└──────────────────────┴───────────────────────────────────────────┘ + Shared (workspace) packages + @edr/types · @edr/api-common · @edr/ui-common · @edr/{eslint,tsconfig,prettier}-config +``` + +### Domain isolation + +- **One database per domain.** `postgres-freight` (port 5433, db `edr_freight`) and `postgres-passenger` (port 5434, db `edr_passenger`). No cross-database joins. Cross-domain data flows only through API calls or message queues. +- **Each domain owns its data model.** Freight bookings/consignments/shipments/trains/invoices/documents live only in the freight DB; passenger journeys/tickets live only in the passenger DB. + +### NestJS module pattern (per feature) + +``` +modules// + entities/.entity.ts // extends BaseEntity (UUID, timestamps, soft delete) + dto/-.dto.ts // class-validator DTOs + .module.ts // wires controller + service + repository + .controller.ts // HTTP layer only — no business logic + .service.ts // business logic + .repository.ts // extends BaseRepository; services inject this, NEVER `Repository` directly +``` + +Conventions enforced across the codebase: + +- All entities have UUID primary keys (`@PrimaryGeneratedColumn('uuid')`). +- All entities inherit `createdAt` / `updatedAt` / `deletedAt` from `BaseEntity` (soft delete). +- DB columns use `snake_case` via `@Column({ name: '...' })`; TS properties stay `camelCase`. +- **No `synchronize: true`** in production — schema changes go through TypeORM migrations. +- ESLint + Prettier run on pre-commit via Husky + lint-staged. +- Conventional-commits enforced via commitlint. + +### Frontend application structure + +``` +apps/edr-freight-web/portal/src/ + App.tsx // routes + sidebar definition + main.tsx // React Router + QueryClient providers + components/ + Breadcrumbs.tsx // shared local UI + ui/ // shadcn-style primitives (Button, Input, Dialog, Label, Textarea) + pages/ + customers/ // CustomersPage + CustomerDetailPage + NewCustomerPage (dialog) + bookings/ // ... + multimodal Transport Legs editor + consignments/ + tracking/ // Shipment grid + table view toggle + trains/ // Fleet roster + billing/ // Invoices + documents/ // MinIO-shaped document library + hooks/ // TanStack Query hooks (per feature) + services/ // Axios clients (per feature) + store/ // Zustand stores + lib/ // utilities (`cn` helper, formatters) +``` + +Each feature folder typically contains: `*Page.tsx` (list), `*DetailPage.tsx`, `New*Page.tsx` (create/edit dialog with `mode: "create" | "edit"`), `Delete*Dialog.tsx`, and a `*.mock.ts` seed file used by the current mock UI. + +### Shared layout (`@edr/ui-common`) + +`DashboardLayout` provides the sidebar + header shell shared across all freight and passenger web apps: + +- **Sidebar** — brand-tinted, icon-led navigation; the brand color (`#33578D`) marks the active item. +- **Header** — language picker, notifications, user dropdown (click-driven, click-outside / Escape close); host apps opt into a light/dark theme toggle via `enableThemeToggle` (Tailwind class-based, persisted in `localStorage`). + +### Auth integration (planned) + +Authentication is provided by an external `@edr/iamui-common` / `@tria-plc/iamapi-common` package. **Do not** implement login/JWT/password logic in this repo. Use placeholder TODO comments next to controllers and `@CurrentUser` decorators (in `@edr/api-common`) until the integration ships. + +--- + +## Apps & Ports + +| App | Package name | Purpose | Port | +| ------------------------------ | --------------------------- | ---------------------------------------- | ---- | +| `edr-freight-api` | `@edr/freight-api` | NestJS API for freight | 3001 | +| `edr-freight-web/portal` | `@edr/freight-portal` | React frontend for freight customers | 5173 | +| `edr-freight-web/backoffice` | `@edr/freight-backoffice` | React frontend for freight employees | 5183 | +| `edr-passenger-api` | `@edr/passenger-api` | NestJS API for passengers | 3002 | +| `edr-passenger-web/portal` | `@edr/passenger-portal` | React frontend for passenger customers | 5174 | +| `edr-passenger-web/backoffice` | `@edr/passenger-backoffice` | React frontend for passenger employees | 5184 | + +`edr-freight-web` and `edr-passenger-web` are grouping folders, not workspace packages. Each holds independent `portal/` and `backoffice/` workspace packages declared in `pnpm-workspace.yaml`. + +--- + +## Getting Started + +### Prerequisites + +- Node.js ≥ 20 +- pnpm 9 (`corepack enable && corepack prepare pnpm@9.12.0 --activate`) +- Docker (for local Postgres) + +### Install + +```bash +pnpm install +``` + +### Start local databases + +```bash +docker compose -f infrastructure/docker/docker-compose.db.dev.yml up -d +``` + +### Run apps + +```bash +pnpm dev # every app +pnpm dev:freight # freight API + portal + backoffice +pnpm dev:passenger # passenger API + portal + backoffice +``` + +### Common scripts + +| Script | Description | +| ------------------- | ------------------------------------ | +| `pnpm install` | Install workspace dependencies | +| `pnpm dev` | Run every app in watch mode | +| `pnpm build` | Build every package and app | +| `pnpm test` | Run all tests | +| `pnpm lint` | Lint everything | +| `pnpm type-check` | Type-check every package | +| `pnpm format` | Format with Prettier | + +--- + +## Repository Layout + +``` +. +├── apps/ +│ ├── edr-freight-api/ NestJS — freight backend +│ ├── edr-freight-web/ +│ │ ├── portal/ React — freight customer portal +│ │ └── backoffice/ React — freight back-office +│ ├── edr-passenger-api/ NestJS — passenger backend +│ └── edr-passenger-web/ +│ ├── portal/ React — passenger customer portal +│ └── backoffice/ React — passenger back-office +├── packages/ +│ ├── api-common/ Shared NestJS utilities + BaseEntity/Repository +│ ├── types/ Shared TS types/enums +│ ├── ui-common/ Shared React components + theme +│ └── config/ +│ ├── eslint/ @edr/eslint-config +│ ├── tsconfig/ @edr/tsconfig +│ └── prettier/ @edr/prettier-config +├── infrastructure/ +│ ├── docker/ docker-compose files (db.dev / dev / prod) +│ └── nginx/ Production nginx config +├── CLAUDE.md Developer guide for AI-assisted work +├── turbo.json Turborepo task pipeline +├── pnpm-workspace.yaml Workspace manifest +└── README.md +``` + +--- + +## Standards (recap) + +- **TypeScript strict mode** is enabled in every package. +- **pnpm only** — never run `npm install` or `yarn`. +- **Conventional commits** — enforced via commitlint on every commit. +- **NestJS 4-layer pattern** — `module → controller → service → repository`. +- **Repository injection** — services inject the custom `*Repository` class, not `Repository`. +- **Controllers are thin** — no business logic; delegate to services. +- **Migrations only** — never enable TypeORM `synchronize` in production. +- **One DB per domain** — no cross-database joins. + +See [`CLAUDE.md`](./CLAUDE.md) for the deeper developer guide used during AI-assisted contributions. diff --git a/apps/edr-freight-web/portal/index.css b/apps/edr-freight-web/portal/index.css index a461c505f..241330c15 100644 --- a/apps/edr-freight-web/portal/index.css +++ b/apps/edr-freight-web/portal/index.css @@ -1 +1,42 @@ -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; + +@custom-variant dark (&:where(.dark, .dark *)); + +* { + scrollbar-width: thin; + scrollbar-color: rgb(203 213 225 / 0.6) transparent; +} + +*::-webkit-scrollbar { + width: 8px; + height: 10px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background-color: rgb(203 213 225 / 0.7); + border-radius: 9999px; +} + +*::-webkit-scrollbar-thumb:hover { + background-color: rgb(51 87 141 / 0.5); +} + +*::-webkit-scrollbar-corner { + background: transparent; +} + +.dark * { + scrollbar-color: rgb(71 85 105 / 0.6) transparent; +} + +.dark *::-webkit-scrollbar-thumb { + background-color: rgb(71 85 105 / 0.6); +} + +.dark *::-webkit-scrollbar-thumb:hover { + background-color: rgb(51 87 141 / 0.7); +} diff --git a/apps/edr-freight-web/portal/package.json b/apps/edr-freight-web/portal/package.json index a0e1de6b0..e3114fb45 100644 --- a/apps/edr-freight-web/portal/package.json +++ b/apps/edr-freight-web/portal/package.json @@ -17,10 +17,15 @@ "@tanstack/react-query": "^5.59.0", "@tria-plc/iamui-common": "1.1.1", "axios": "^1.7.7", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "lucide-react": "^1.14.0", + "radix-ui": "^1.4.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.27.0", + "recharts": "^3.8.1", + "tailwind-merge": "^3.6.0", "zustand": "^5.0.0" }, "devDependencies": { diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx index e1482c142..c39581506 100644 --- a/apps/edr-freight-web/portal/src/App.tsx +++ b/apps/edr-freight-web/portal/src/App.tsx @@ -6,10 +6,21 @@ import { Navigate, } from "react-router-dom"; import { DashboardLayout, type SidebarItem } from "@edr/ui-common"; +import { + LayoutDashboard, + Users, + CalendarCheck, + Package, + MapPin, + Train, + Receipt, + FileText, + Settings, +} from "lucide-react"; import BookingsPage from "./pages/bookings/BookingsPage"; import BookingDetailPage from "./pages/bookings/BookingDetailPage"; -import CreateBookingPage from "./pages/bookings/CreateBookingPage"; +import NewBookingPage from "./pages/bookings/NewBookingPage"; import ConsignmentsPage from "./pages/consignments/ConsignmentsPage"; import ConsignmentDetailPage from "./pages/consignments/ConsignmentDetailPage"; import TrackingPage from "./pages/tracking/TrackingPage"; @@ -17,14 +28,22 @@ import BillingPage from "./pages/billing/BillingPage"; import TrainsPage from "./pages/trains/TrainsPage"; import DashboardPage from "./pages/dashboard/DashboardPage"; import { IamLoginPage, LoadingScreen, useAuth } from "@tria-plc/iamui-common"; +import CustomersPage from "./pages/customers/CustomersPage"; +import CustomerDetailPage from "./pages/customers/CustomerDetailPage"; +import NewCustomerPage from "./pages/customers/NewCustomerPage"; +import DocumentsPage from "./pages/documents/DocumentsPage"; +import DropdownSettingsPage from "./pages/admin/DropdownSettingsPage"; 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" }, + { label: "Dashboard", href: "/", icon: }, + { label: "Customers", href: "/customers", icon: }, + { label: "Bookings", href: "/bookings", icon: }, + { label: "Consignments", href: "/consignments", icon: }, + { label: "Tracking", href: "/tracking", icon: }, + { label: "Trains", href: "/trains", icon: }, + { label: "Billing", href: "/billing", icon: }, + { label: "Documents", href: "/documents", icon: }, + { label: "Dropdown Settings", href: "/admin/dropdowns", icon: }, ]; const App = () => { @@ -55,17 +74,26 @@ const App = () => { sidebarItems={sidebarItems} activeHref={location.pathname} onNavigate={navigate} + enableThemeToggle > } /> } /> - } /> + } /> + } /> + } /> + } /> } /> } /> } /> } /> } /> } /> + } /> + } + /> } /> diff --git a/apps/edr-freight-web/portal/src/components/Breadcrumbs.tsx b/apps/edr-freight-web/portal/src/components/Breadcrumbs.tsx new file mode 100644 index 000000000..159d7eead --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/Breadcrumbs.tsx @@ -0,0 +1,56 @@ +import { Fragment } from "react"; +import { Link } from "react-router-dom"; +import { ChevronRight, Home } from "lucide-react"; + +export interface BreadcrumbItem { + label: string; + href?: string; +} + +export interface BreadcrumbsProps { + items: BreadcrumbItem[]; +} + +export default function Breadcrumbs({ items }: BreadcrumbsProps) { + return ( + + ); +} diff --git a/apps/edr-freight-web/portal/src/components/ui/button.tsx b/apps/edr-freight-web/portal/src/components/ui/button.tsx new file mode 100644 index 000000000..1d9f2e228 --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/ui/button.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; + +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot.Root : "button"; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/apps/edr-freight-web/portal/src/components/ui/dialog.tsx b/apps/edr-freight-web/portal/src/components/ui/dialog.tsx new file mode 100644 index 000000000..5bf59ed79 --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import * as React from "react"; +import { Dialog as DialogPrimitive } from "radix-ui"; +import { XIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +function Dialog({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean; +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/apps/edr-freight-web/portal/src/components/ui/input.tsx b/apps/edr-freight-web/portal/src/components/ui/input.tsx new file mode 100644 index 000000000..311be213b --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/apps/edr-freight-web/portal/src/components/ui/label.tsx b/apps/edr-freight-web/portal/src/components/ui/label.tsx new file mode 100644 index 000000000..61818eba8 --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { Label as LabelPrimitive } from "radix-ui"; + +import { cn } from "@/lib/utils"; + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Label }; diff --git a/apps/edr-freight-web/portal/src/components/ui/textarea.tsx b/apps/edr-freight-web/portal/src/components/ui/textarea.tsx new file mode 100644 index 000000000..2e6c1f849 --- /dev/null +++ b/apps/edr-freight-web/portal/src/components/ui/textarea.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { + return ( +