chore: fmt

This commit is contained in:
Michael Abebe
2026-05-12 16:50:18 +03:00
parent 4540d35215
commit 1c5ee19388
181 changed files with 1492 additions and 1100 deletions

View File

@@ -1,44 +1,49 @@
# EDR Platform — Developer Guide # EDR Platform — Developer Guide
## Overview ## Overview
Monorepo for the Ethio Djibouti Railway (EDR) digital platform. Contains the Freight Management and Passenger Management applications, plus shared types, NestJS utilities, and React component libraries. Monorepo for the Ethio Djibouti Railway (EDR) digital platform. Contains the Freight Management and Passenger Management applications, plus shared types, NestJS utilities, and React component libraries.
## Apps ## Apps
| App | Package name | Purpose | Port |
|---|---|---|---| | App | Package name | Purpose | Port |
| `edr-freight-api` | `@edr/freight-api` | NestJS API for freight management | 3001 | | ------------------------------ | --------------------------- | -------------------------------------------------- | ---- |
| `edr-freight-web/portal` | `@edr/freight-portal` | React frontend for freight customer/portal users | 5173 | | `edr-freight-api` | `@edr/freight-api` | NestJS API for freight management | 3001 |
| `edr-freight-web/backoffice` | `@edr/freight-backoffice` | React frontend for freight backoffice employees | 5183 | | `edr-freight-web/portal` | `@edr/freight-portal` | React frontend for freight customer/portal users | 5173 |
| `edr-passenger-api` | `@edr/passenger-api` | NestJS API for passenger management | 3002 | | `edr-freight-web/backoffice` | `@edr/freight-backoffice` | React frontend for freight backoffice employees | 5183 |
| `edr-passenger-web/portal` | `@edr/passenger-portal` | React frontend for passenger customer/portal users | 5174 | | `edr-passenger-api` | `@edr/passenger-api` | NestJS API for passenger management | 3002 |
| `edr-passenger-web/backoffice` | `@edr/passenger-backoffice` | React frontend for passenger backoffice employees | 5184 | | `edr-passenger-web/portal` | `@edr/passenger-portal` | React frontend for passenger customer/portal users | 5174 |
| `edr-passenger-web/backoffice` | `@edr/passenger-backoffice` | React frontend for passenger backoffice employees | 5184 |
`edr-freight-web` and `edr-passenger-web` are grouping folders, not workspace packages. Each holds a `portal/` and `backoffice/` sub-app, both of which are independent pnpm workspace packages (declared in `pnpm-workspace.yaml`). The existing `pnpm dev:freight` / `pnpm dev:passenger` turbo filters (`@edr/freight-*` / `@edr/passenger-*`) cover all four web apps + their APIs. `edr-freight-web` and `edr-passenger-web` are grouping folders, not workspace packages. Each holds a `portal/` and `backoffice/` sub-app, both of which are independent pnpm workspace packages (declared in `pnpm-workspace.yaml`). The existing `pnpm dev:freight` / `pnpm dev:passenger` turbo filters (`@edr/freight-*` / `@edr/passenger-*`) cover all four web apps + their APIs.
## Packages ## Packages
| Package | Purpose |
|---|---| | Package | Purpose |
| `@edr/types` | Shared TypeScript interfaces and enums | | ---------------------- | ---------------------------------------------------------------------------------- |
| `@edr/api-common` | Shared NestJS decorators, filters, interceptors, pipes, BaseEntity, BaseRepository | | `@edr/types` | Shared TypeScript interfaces and enums |
| `@edr/ui-common` | Shared React components and theme | | `@edr/api-common` | Shared NestJS decorators, filters, interceptors, pipes, BaseEntity, BaseRepository |
| `@edr/eslint-config` | Shared ESLint configurations (base/nestjs/react) | | `@edr/ui-common` | Shared React components and theme |
| `@edr/tsconfig` | Shared TypeScript configurations | | `@edr/eslint-config` | Shared ESLint configurations (base/nestjs/react) |
| `@edr/prettier-config` | Shared Prettier configuration | | `@edr/tsconfig` | Shared TypeScript configurations |
| `@edr/prettier-config` | Shared Prettier configuration |
## Commands ## Commands
| Command | Description |
|---|---| | Command | Description |
| `pnpm install` | Install all workspace dependencies | | -------------------- | ---------------------------------- |
| `pnpm dev` | Run every app in dev mode | | `pnpm install` | Install all workspace dependencies |
| `pnpm dev:freight` | Run only freight API + web | | `pnpm dev` | Run every app in dev mode |
| `pnpm dev:passenger` | Run only passenger API + web | | `pnpm dev:freight` | Run only freight API + web |
| `pnpm build` | Build every package and app | | `pnpm dev:passenger` | Run only passenger API + web |
| `pnpm test` | Run all tests | | `pnpm build` | Build every package and app |
| `pnpm lint` | Lint everything | | `pnpm test` | Run all tests |
| `pnpm type-check` | Type-check every package | | `pnpm lint` | Lint everything |
| `pnpm format` | Format all files with Prettier | | `pnpm type-check` | Type-check every package |
| `pnpm format` | Format all files with Prettier |
## Standards ## Standards
- **TypeScript strict mode** is enabled in every package and app. - **TypeScript strict mode** is enabled in every package and app.
- **pnpm** is the only supported package manager — never run `npm install` or `yarn`. - **pnpm** is the only supported package manager — never run `npm install` or `yarn`.
- **Conventional commits** are enforced via commitlint on every commit. - **Conventional commits** are enforced via commitlint on every commit.
@@ -52,15 +57,18 @@ Monorepo for the Ethio Djibouti Railway (EDR) digital platform. Contains the Fre
- **Controllers** never contain business logic. - **Controllers** never contain business logic.
## Auth ## Auth
Authentication is handled by an external package (`@edr/iamui-common` or equivalent) that will be integrated later. **Do not** implement any auth, login, logout, JWT verification, password hashing, or user management code in this repo. Authentication is handled by an external package (`@edr/iamui-common` or equivalent) that will be integrated later. **Do not** implement any auth, login, logout, JWT verification, password hashing, or user management code in this repo.
When auth integration is needed, use placeholder TODO comments: When auth integration is needed, use placeholder TODO comments:
- `// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth` - `// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth`
- `// TODO: integrate @edr/auth — replace stub @CurrentUser with real one` - `// TODO: integrate @edr/auth — replace stub @CurrentUser with real one`
The `@CurrentUser`, `@Roles`, and `@Public` decorators in `@edr/api-common` are bare metadata setters with no guard wiring — they exist so controllers can be annotated correctly without depending on auth infrastructure yet. The `@CurrentUser`, `@Roles`, and `@Public` decorators in `@edr/api-common` are bare metadata setters with no guard wiring — they exist so controllers can be annotated correctly without depending on auth infrastructure yet.
## Port Assignments ## Port Assignments
- `edr-freight-api`: 3001 - `edr-freight-api`: 3001
- `edr-freight-web/portal`: 5173 - `edr-freight-web/portal`: 5173
- `edr-freight-web/backoffice`: 5183 - `edr-freight-web/backoffice`: 5183
@@ -69,11 +77,13 @@ The `@CurrentUser`, `@Roles`, and `@Public` decorators in `@edr/api-common` are
- `edr-passenger-web/backoffice`: 5184 - `edr-passenger-web/backoffice`: 5184
## Database Layout ## Database Layout
- `postgres-freight` (port 5433): database `edr_freight` — freight API only. - `postgres-freight` (port 5433): database `edr_freight` — freight API only.
- `postgres-passenger` (port 5434): database `edr_passenger` — passenger API only. - `postgres-passenger` (port 5434): database `edr_passenger` — passenger API only.
- Each app owns its own DB. No cross-database joins; cross-domain data flows through API calls or message queues. - Each app owns its own DB. No cross-database joins; cross-domain data flows through API calls or message queues.
## Adding a new module to a NestJS app ## Adding a new module to a NestJS app
1. Create `modules/<feature>/` with `entities/`, `dto/`, and the four `<feature>.{module,controller,service,repository}.ts` files. 1. Create `modules/<feature>/` with `entities/`, `dto/`, and the four `<feature>.{module,controller,service,repository}.ts` files.
2. The entity extends `BaseEntity` from `@edr/api-common`. 2. The entity extends `BaseEntity` from `@edr/api-common`.
3. The repository extends `BaseRepository<Entity>` from `@edr/api-common`. 3. The repository extends `BaseRepository<Entity>` from `@edr/api-common`.
@@ -82,6 +92,7 @@ The `@CurrentUser`, `@Roles`, and `@Public` decorators in `@edr/api-common` are
6. Register the module in the app's `app.module.ts`. 6. Register the module in the app's `app.module.ts`.
## Adding a new shared component to `@edr/ui-common` ## Adding a new shared component to `@edr/ui-common`
1. Create `src/components/<Name>/<Name>.tsx` and `src/components/<Name>/index.ts`. 1. Create `src/components/<Name>/<Name>.tsx` and `src/components/<Name>/index.ts`.
2. Export from `src/index.ts`. 2. Export from `src/index.ts`.
3. Component is a functional component with a `ComponentNameProps` interface (named-exported alongside the default). 3. Component is a functional component with a `ComponentNameProps` interface (named-exported alongside the default).

File diff suppressed because one or more lines are too long

View File

@@ -52,11 +52,19 @@
"typescript": "^5.5.4" "typescript": "^5.5.4"
}, },
"jest": { "jest": {
"moduleFileExtensions": ["js", "json", "ts"], "moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src", "rootDir": "src",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "^.+\\.(t|j)s$": "ts-jest" }, "transform": {
"collectCoverageFrom": ["**/*.(t|j)s"], "^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
} }

View File

@@ -1,17 +1,17 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
import appConfig from './config/app.config'; import appConfig from "./config/app.config";
import databaseConfig from './config/database.config'; import databaseConfig from "./config/database.config";
import { BookingsModule } from './modules/bookings/bookings.module'; import { BookingsModule } from "./modules/bookings/bookings.module";
import { ConsignmentsModule } from './modules/consignments/consignments.module'; import { ConsignmentsModule } from "./modules/consignments/consignments.module";
import { TrainsModule } from './modules/trains/trains.module'; import { TrainsModule } from "./modules/trains/trains.module";
import { CustomersModule } from './modules/customers/customers.module'; import { CustomersModule } from "./modules/customers/customers.module";
import { TrackingModule } from './modules/tracking/tracking.module'; import { TrackingModule } from "./modules/tracking/tracking.module";
import { BillingModule } from './modules/billing/billing.module'; import { BillingModule } from "./modules/billing/billing.module";
import { NotificationsModule } from './modules/notifications/notifications.module'; import { NotificationsModule } from "./modules/notifications/notifications.module";
@Module({ @Module({
imports: [ imports: [
@@ -22,7 +22,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
inject: [ConfigService], inject: [ConfigService],
useFactory: (config: ConfigService): TypeOrmModuleOptions => useFactory: (config: ConfigService): TypeOrmModuleOptions =>
config.get<TypeOrmModuleOptions>('database')!, config.get<TypeOrmModuleOptions>("database")!,
}), }),
BookingsModule, BookingsModule,
ConsignmentsModule, ConsignmentsModule,

View File

@@ -1 +1 @@
export { HttpExceptionFilter } from '@edr/api-common'; export { HttpExceptionFilter } from "@edr/api-common";

View File

@@ -1 +1 @@
export { ResponseTransformInterceptor } from '@edr/api-common'; export { ResponseTransformInterceptor } from "@edr/api-common";

View File

@@ -1 +1 @@
export { createValidationPipe } from '@edr/api-common'; export { createValidationPipe } from "@edr/api-common";

View File

@@ -1,7 +1,7 @@
import { registerAs } from '@nestjs/config'; import { registerAs } from "@nestjs/config";
export default registerAs('app', () => ({ export default registerAs("app", () => ({
env: process.env.NODE_ENV ?? 'development', env: process.env.NODE_ENV ?? "development",
port: parseInt(process.env.PORT ?? '3001', 10), port: parseInt(process.env.PORT ?? "3001", 10),
apiPrefix: 'api', apiPrefix: "api",
})); }));

View File

@@ -1,16 +1,19 @@
import { registerAs } from '@nestjs/config'; import { registerAs } from "@nestjs/config";
import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { TypeOrmModuleOptions } from "@nestjs/typeorm";
export default registerAs('database', (): TypeOrmModuleOptions => ({ export default registerAs(
type: 'postgres', "database",
host: process.env.DB_HOST ?? 'localhost', (): TypeOrmModuleOptions => ({
port: parseInt(process.env.DB_PORT ?? '5433', 10), type: "postgres",
username: process.env.DB_USER ?? 'postgres', host: process.env.DB_HOST ?? "localhost",
password: process.env.DB_PASSWORD ?? '', port: parseInt(process.env.DB_PORT ?? "5433", 10),
database: process.env.DB_NAME ?? 'edr_freight', username: process.env.DB_USER ?? "postgres",
entities: [__dirname + '/../**/*.entity.{ts,js}'], password: process.env.DB_PASSWORD ?? "",
migrations: [__dirname + '/../../migrations/*.{ts,js}'], database: process.env.DB_NAME ?? "edr_freight",
// Never enable synchronize in production. Use migrations. entities: [__dirname + "/../**/*.entity.{ts,js}"],
synchronize: process.env.NODE_ENV === 'development', migrations: [__dirname + "/../../migrations/*.{ts,js}"],
logging: process.env.NODE_ENV === 'development', // Never enable synchronize in production. Use migrations.
})); synchronize: process.env.NODE_ENV === "development",
logging: process.env.NODE_ENV === "development",
}),
);

View File

@@ -1,28 +1,32 @@
import 'reflect-metadata'; import "reflect-metadata";
import { NestFactory } from '@nestjs/core'; import { NestFactory } from "@nestjs/core";
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { HttpExceptionFilter, ResponseTransformInterceptor, createValidationPipe } from '@edr/api-common'; import {
HttpExceptionFilter,
ResponseTransformInterceptor,
createValidationPipe,
} from "@edr/api-common";
import { AppModule } from './app.module'; import { AppModule } from "./app.module";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true }); const app = await NestFactory.create(AppModule, { cors: true });
app.setGlobalPrefix('api'); app.setGlobalPrefix("api");
app.useGlobalPipes(createValidationPipe()); app.useGlobalPipes(createValidationPipe());
app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResponseTransformInterceptor()); app.useGlobalInterceptors(new ResponseTransformInterceptor());
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('EDR Freight API') .setTitle("EDR Freight API")
.setDescription('API for the EDR Freight Management application') .setDescription("API for the EDR Freight Management application")
.setVersion('0.1.0') .setVersion("0.1.0")
.addBearerAuth() .addBearerAuth()
.build(); .build();
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document); SwaggerModule.setup("api/docs", app, document);
const port = parseInt(process.env.PORT ?? '3001', 10); const port = parseInt(process.env.PORT ?? "3001", 10);
await app.listen(port); await app.listen(port);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`[freight-api] listening on http://localhost:${port}`); console.log(`[freight-api] listening on http://localhost:${port}`);

View File

@@ -1,23 +1,23 @@
import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common'; import { Controller, Get, Param, ParseUUIDPipe } from "@nestjs/common";
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { BillingService } from './billing.service'; import { BillingService } from "./billing.service";
@ApiTags('billing') @ApiTags("billing")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth // @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller('billing') @Controller("billing")
export class BillingController { export class BillingController {
constructor(private readonly billingService: BillingService) {} constructor(private readonly billingService: BillingService) {}
@Get('invoices') @Get("invoices")
@ApiOperation({ summary: 'List all invoices' }) @ApiOperation({ summary: "List all invoices" })
findAll() { findAll() {
return this.billingService.findAll(); return this.billingService.findAll();
} }
@Get('invoices/booking/:bookingId') @Get("invoices/booking/:bookingId")
@ApiOperation({ summary: 'List invoices for a booking' }) @ApiOperation({ summary: "List invoices for a booking" })
findByBooking(@Param('bookingId', ParseUUIDPipe) bookingId: string) { findByBooking(@Param("bookingId", ParseUUIDPipe) bookingId: string) {
return this.billingService.findByBooking(bookingId); return this.billingService.findByBooking(bookingId);
} }
} }

View File

@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { BillingController } from './billing.controller'; import { BillingController } from "./billing.controller";
import { BillingService } from './billing.service'; import { BillingService } from "./billing.service";
import { Invoice } from './entities/invoice.entity'; import { Invoice } from "./entities/invoice.entity";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Invoice])], imports: [TypeOrmModule.forFeature([Invoice])],

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { Invoice } from './entities/invoice.entity'; import { Invoice } from "./entities/invoice.entity";
@Injectable() @Injectable()
export class BillingService { export class BillingService {
@@ -13,11 +13,14 @@ export class BillingService {
/** List every invoice (most recent first). */ /** List every invoice (most recent first). */
findAll(): Promise<Invoice[]> { findAll(): Promise<Invoice[]> {
return this.invoicesRepository.find({ order: { issuedAt: 'DESC' } }); return this.invoicesRepository.find({ order: { issuedAt: "DESC" } });
} }
/** List invoices for a given booking. */ /** List invoices for a given booking. */
findByBooking(bookingId: string): Promise<Invoice[]> { findByBooking(bookingId: string): Promise<Invoice[]> {
return this.invoicesRepository.find({ where: { bookingId }, order: { issuedAt: 'DESC' } }); return this.invoicesRepository.find({
where: { bookingId },
order: { issuedAt: "DESC" },
});
} }
} }

View File

@@ -1,32 +1,32 @@
import { BaseEntity } from '@edr/api-common'; import { BaseEntity } from "@edr/api-common";
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
@Entity({ name: 'invoices' }) @Entity({ name: "invoices" })
export class Invoice extends BaseEntity { export class Invoice extends BaseEntity {
@Column({ name: 'booking_id', type: 'uuid' }) @Column({ name: "booking_id", type: "uuid" })
bookingId!: string; bookingId!: string;
@Column({ name: 'invoice_number', type: 'varchar', length: 64, unique: true }) @Column({ name: "invoice_number", type: "varchar", length: 64, unique: true })
invoiceNumber!: string; invoiceNumber!: string;
@Column({ name: 'amount', type: 'numeric', precision: 14, scale: 2 }) @Column({ name: "amount", type: "numeric", precision: 14, scale: 2 })
amount!: number; amount!: number;
@Column({ name: 'currency', type: 'varchar', length: 8, default: 'ETB' }) @Column({ name: "currency", type: "varchar", length: 8, default: "ETB" })
currency!: string; currency!: string;
@Column({ @Column({
name: 'status', name: "status",
type: 'enum', type: "enum",
enum: Freight.PaymentStatus, enum: Freight.PaymentStatus,
default: Freight.PaymentStatus.Pending, default: Freight.PaymentStatus.Pending,
}) })
status!: Freight.PaymentStatus; status!: Freight.PaymentStatus;
@Column({ name: 'issued_at', type: 'timestamptz' }) @Column({ name: "issued_at", type: "timestamptz" })
issuedAt!: Date; issuedAt!: Date;
@Column({ name: 'due_at', type: 'timestamptz' }) @Column({ name: "due_at", type: "timestamptz" })
dueAt!: Date; dueAt!: Date;
} }

View File

@@ -1,38 +1,48 @@
import { Body, Controller, Delete, Get, HttpCode, Param, ParseUUIDPipe, Post, Query } from '@nestjs/common'; import {
import { ApiOperation, ApiTags } from '@nestjs/swagger'; Body,
Controller,
Delete,
Get,
HttpCode,
Param,
ParseUUIDPipe,
Post,
Query,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { BookingsService } from './bookings.service'; import { BookingsService } from "./bookings.service";
import { CreateBookingDto } from './dto/create-booking.dto'; import { CreateBookingDto } from "./dto/create-booking.dto";
import { FilterBookingDto } from './dto/filter-booking.dto'; import { FilterBookingDto } from "./dto/filter-booking.dto";
@ApiTags('bookings') @ApiTags("bookings")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth // @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller('bookings') @Controller("bookings")
export class BookingsController { export class BookingsController {
constructor(private readonly bookingsService: BookingsService) {} constructor(private readonly bookingsService: BookingsService) {}
@Post() @Post()
@ApiOperation({ summary: 'Create a new freight booking' }) @ApiOperation({ summary: "Create a new freight booking" })
create(@Body() dto: CreateBookingDto) { create(@Body() dto: CreateBookingDto) {
return this.bookingsService.create(dto); return this.bookingsService.create(dto);
} }
@Get() @Get()
@ApiOperation({ summary: 'List freight bookings (paginated)' }) @ApiOperation({ summary: "List freight bookings (paginated)" })
findAll(@Query() filter: FilterBookingDto) { findAll(@Query() filter: FilterBookingDto) {
return this.bookingsService.findAll(filter); return this.bookingsService.findAll(filter);
} }
@Get(':id') @Get(":id")
@ApiOperation({ summary: 'Get a freight booking by ID' }) @ApiOperation({ summary: "Get a freight booking by ID" })
findOne(@Param('id', ParseUUIDPipe) id: string) { findOne(@Param("id", ParseUUIDPipe) id: string) {
return this.bookingsService.findById(id); return this.bookingsService.findById(id);
} }
@Delete(':id') @Delete(":id")
@HttpCode(204) @HttpCode(204)
@ApiOperation({ summary: 'Soft-delete a freight booking' }) @ApiOperation({ summary: "Soft-delete a freight booking" })
remove(@Param('id', ParseUUIDPipe) id: string) { remove(@Param("id", ParseUUIDPipe) id: string) {
return this.bookingsService.remove(id); return this.bookingsService.remove(id);
} }
} }

View File

@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { BookingsController } from './bookings.controller'; import { BookingsController } from "./bookings.controller";
import { BookingsRepository } from './bookings.repository'; import { BookingsRepository } from "./bookings.repository";
import { BookingsService } from './bookings.service'; import { BookingsService } from "./bookings.service";
import { Booking } from './entities/booking.entity'; import { Booking } from "./entities/booking.entity";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Booking])], imports: [TypeOrmModule.forFeature([Booking])],

View File

@@ -1,9 +1,9 @@
import { BaseRepository } from '@edr/api-common'; import { BaseRepository } from "@edr/api-common";
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { Booking } from './entities/booking.entity'; import { Booking } from "./entities/booking.entity";
@Injectable() @Injectable()
export class BookingsRepository extends BaseRepository<Booking> { export class BookingsRepository extends BaseRepository<Booking> {

View File

@@ -1,9 +1,9 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from "@nestjs/common";
import { BookingsRepository } from './bookings.repository'; import { BookingsRepository } from "./bookings.repository";
import { CreateBookingDto } from './dto/create-booking.dto'; import { CreateBookingDto } from "./dto/create-booking.dto";
import { FilterBookingDto } from './dto/filter-booking.dto'; import { FilterBookingDto } from "./dto/filter-booking.dto";
import { Booking } from './entities/booking.entity'; import { Booking } from "./entities/booking.entity";
@Injectable() @Injectable()
export class BookingsService { export class BookingsService {
@@ -18,7 +18,9 @@ export class BookingsService {
} }
/** Return a paginated list of bookings matching the filter. */ /** Return a paginated list of bookings matching the filter. */
async findAll(filter: FilterBookingDto): Promise<{ items: Booking[]; total: number }> { async findAll(
filter: FilterBookingDto,
): Promise<{ items: Booking[]; total: number }> {
const page = filter.page ?? 1; const page = filter.page ?? 1;
const pageSize = filter.pageSize ?? 20; const pageSize = filter.pageSize ?? 20;
const [items, total] = await this.bookingsRepository.findAndCount({ const [items, total] = await this.bookingsRepository.findAndCount({
@@ -28,7 +30,7 @@ export class BookingsService {
}, },
skip: (page - 1) * pageSize, skip: (page - 1) * pageSize,
take: pageSize, take: pageSize,
order: { createdAt: 'DESC' }, order: { createdAt: "DESC" },
}); });
return { items, total }; return { items, total };
} }

View File

@@ -1,4 +1,4 @@
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { import {
IsDateString, IsDateString,
IsEnum, IsEnum,
@@ -7,7 +7,7 @@ import {
IsString, IsString,
IsUUID, IsUUID,
Min, Min,
} from 'class-validator'; } from "class-validator";
export class CreateBookingDto { export class CreateBookingDto {
@IsString() @IsString()

View File

@@ -1,6 +1,6 @@
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { Type } from 'class-transformer'; import { Type } from "class-transformer";
import { IsEnum, IsInt, IsOptional, IsUUID, Min } from 'class-validator'; import { IsEnum, IsInt, IsOptional, IsUUID, Min } from "class-validator";
export class FilterBookingDto { export class FilterBookingDto {
@IsOptional() @IsOptional()

View File

@@ -1,35 +1,41 @@
import { BaseEntity } from '@edr/api-common'; import { BaseEntity } from "@edr/api-common";
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
@Entity({ name: 'bookings' }) @Entity({ name: "bookings" })
export class Booking extends BaseEntity { export class Booking extends BaseEntity {
@Column({ name: 'reference', type: 'varchar', length: 64, unique: true }) @Column({ name: "reference", type: "varchar", length: 64, unique: true })
reference!: string; reference!: string;
@Column({ name: 'customer_id', type: 'uuid' }) @Column({ name: "customer_id", type: "uuid" })
customerId!: string; customerId!: string;
@Column({ name: 'train_id', type: 'uuid', nullable: true }) @Column({ name: "train_id", type: "uuid", nullable: true })
trainId?: string | null; trainId?: string | null;
@Column({ @Column({
name: 'status', name: "status",
type: 'enum', type: "enum",
enum: Freight.BookingStatus, enum: Freight.BookingStatus,
default: Freight.BookingStatus.Draft, default: Freight.BookingStatus.Draft,
}) })
status!: Freight.BookingStatus; status!: Freight.BookingStatus;
@Column({ name: 'scheduled_date', type: 'timestamptz' }) @Column({ name: "scheduled_date", type: "timestamptz" })
scheduledDate!: Date; scheduledDate!: Date;
@Column({ name: 'total_amount', type: 'numeric', precision: 14, scale: 2, default: 0 }) @Column({
name: "total_amount",
type: "numeric",
precision: 14,
scale: 2,
default: 0,
})
totalAmount!: number; totalAmount!: number;
@Column({ @Column({
name: 'payment_status', name: "payment_status",
type: 'enum', type: "enum",
enum: Freight.PaymentStatus, enum: Freight.PaymentStatus,
default: Freight.PaymentStatus.Pending, default: Freight.PaymentStatus.Pending,
}) })

View File

@@ -1,31 +1,39 @@
import { Body, Controller, Get, Param, ParseUUIDPipe, Post, Query } from '@nestjs/common'; import {
import { ApiOperation, ApiTags } from '@nestjs/swagger'; Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
Query,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { ConsignmentsService } from './consignments.service'; import { ConsignmentsService } from "./consignments.service";
import { CreateConsignmentDto } from './dto/create-consignment.dto'; import { CreateConsignmentDto } from "./dto/create-consignment.dto";
import { FilterConsignmentDto } from './dto/filter-consignment.dto'; import { FilterConsignmentDto } from "./dto/filter-consignment.dto";
@ApiTags('consignments') @ApiTags("consignments")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth // @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller('consignments') @Controller("consignments")
export class ConsignmentsController { export class ConsignmentsController {
constructor(private readonly consignmentsService: ConsignmentsService) {} constructor(private readonly consignmentsService: ConsignmentsService) {}
@Post() @Post()
@ApiOperation({ summary: 'Create a new consignment' }) @ApiOperation({ summary: "Create a new consignment" })
create(@Body() dto: CreateConsignmentDto) { create(@Body() dto: CreateConsignmentDto) {
return this.consignmentsService.create(dto); return this.consignmentsService.create(dto);
} }
@Get() @Get()
@ApiOperation({ summary: 'List consignments (paginated)' }) @ApiOperation({ summary: "List consignments (paginated)" })
findAll(@Query() filter: FilterConsignmentDto) { findAll(@Query() filter: FilterConsignmentDto) {
return this.consignmentsService.findAll(filter); return this.consignmentsService.findAll(filter);
} }
@Get(':id') @Get(":id")
@ApiOperation({ summary: 'Get a consignment by ID' }) @ApiOperation({ summary: "Get a consignment by ID" })
findOne(@Param('id', ParseUUIDPipe) id: string) { findOne(@Param("id", ParseUUIDPipe) id: string) {
return this.consignmentsService.findById(id); return this.consignmentsService.findById(id);
} }
} }

View File

@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { ConsignmentsController } from './consignments.controller'; import { ConsignmentsController } from "./consignments.controller";
import { ConsignmentsRepository } from './consignments.repository'; import { ConsignmentsRepository } from "./consignments.repository";
import { ConsignmentsService } from './consignments.service'; import { ConsignmentsService } from "./consignments.service";
import { Consignment } from './entities/consignment.entity'; import { Consignment } from "./entities/consignment.entity";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Consignment])], imports: [TypeOrmModule.forFeature([Consignment])],

View File

@@ -1,9 +1,9 @@
import { BaseRepository } from '@edr/api-common'; import { BaseRepository } from "@edr/api-common";
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { Consignment } from './entities/consignment.entity'; import { Consignment } from "./entities/consignment.entity";
@Injectable() @Injectable()
export class ConsignmentsRepository extends BaseRepository<Consignment> { export class ConsignmentsRepository extends BaseRepository<Consignment> {

View File

@@ -1,13 +1,15 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from "@nestjs/common";
import { ConsignmentsRepository } from './consignments.repository'; import { ConsignmentsRepository } from "./consignments.repository";
import { CreateConsignmentDto } from './dto/create-consignment.dto'; import { CreateConsignmentDto } from "./dto/create-consignment.dto";
import { FilterConsignmentDto } from './dto/filter-consignment.dto'; import { FilterConsignmentDto } from "./dto/filter-consignment.dto";
import { Consignment } from './entities/consignment.entity'; import { Consignment } from "./entities/consignment.entity";
@Injectable() @Injectable()
export class ConsignmentsService { export class ConsignmentsService {
constructor(private readonly consignmentsRepository: ConsignmentsRepository) {} constructor(
private readonly consignmentsRepository: ConsignmentsRepository,
) {}
/** Create a new consignment for a freight booking. */ /** Create a new consignment for a freight booking. */
create(dto: CreateConsignmentDto): Promise<Consignment> { create(dto: CreateConsignmentDto): Promise<Consignment> {
@@ -15,7 +17,9 @@ export class ConsignmentsService {
} }
/** Return a paginated list of consignments. */ /** Return a paginated list of consignments. */
async findAll(filter: FilterConsignmentDto): Promise<{ items: Consignment[]; total: number }> { async findAll(
filter: FilterConsignmentDto,
): Promise<{ items: Consignment[]; total: number }> {
const page = filter.page ?? 1; const page = filter.page ?? 1;
const pageSize = filter.pageSize ?? 20; const pageSize = filter.pageSize ?? 20;
const [items, total] = await this.consignmentsRepository.findAndCount({ const [items, total] = await this.consignmentsRepository.findAndCount({
@@ -25,7 +29,7 @@ export class ConsignmentsService {
}, },
skip: (page - 1) * pageSize, skip: (page - 1) * pageSize,
take: pageSize, take: pageSize,
order: { createdAt: 'DESC' }, order: { createdAt: "DESC" },
}); });
return { items, total }; return { items, total };
} }

View File

@@ -1,5 +1,5 @@
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { IsEnum, IsNumber, IsString, IsUUID, Min } from 'class-validator'; import { IsEnum, IsNumber, IsString, IsUUID, Min } from "class-validator";
export class CreateConsignmentDto { export class CreateConsignmentDto {
@IsUUID() @IsUUID()

View File

@@ -1,6 +1,6 @@
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { Type } from 'class-transformer'; import { Type } from "class-transformer";
import { IsEnum, IsInt, IsOptional, IsUUID, Min } from 'class-validator'; import { IsEnum, IsInt, IsOptional, IsUUID, Min } from "class-validator";
export class FilterConsignmentDto { export class FilterConsignmentDto {
@IsOptional() @IsOptional()

View File

@@ -1,32 +1,37 @@
import { BaseEntity } from '@edr/api-common'; import { BaseEntity } from "@edr/api-common";
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
@Entity({ name: 'consignments' }) @Entity({ name: "consignments" })
export class Consignment extends BaseEntity { export class Consignment extends BaseEntity {
@Column({ name: 'booking_id', type: 'uuid' }) @Column({ name: "booking_id", type: "uuid" })
bookingId!: string; bookingId!: string;
@Column({ name: 'tracking_number', type: 'varchar', length: 64, unique: true }) @Column({
name: "tracking_number",
type: "varchar",
length: 64,
unique: true,
})
trackingNumber!: string; trackingNumber!: string;
@Column({ name: 'cargo_type', type: 'enum', enum: Freight.CargoType }) @Column({ name: "cargo_type", type: "enum", enum: Freight.CargoType })
cargoType!: Freight.CargoType; cargoType!: Freight.CargoType;
@Column({ name: 'weight_kg', type: 'numeric', precision: 12, scale: 2 }) @Column({ name: "weight_kg", type: "numeric", precision: 12, scale: 2 })
weightKg!: number; weightKg!: number;
@Column({ @Column({
name: 'status', name: "status",
type: 'enum', type: "enum",
enum: Freight.ConsignmentStatus, enum: Freight.ConsignmentStatus,
default: Freight.ConsignmentStatus.Pending, default: Freight.ConsignmentStatus.Pending,
}) })
status!: Freight.ConsignmentStatus; status!: Freight.ConsignmentStatus;
@Column({ name: 'origin_station', type: 'varchar', length: 128 }) @Column({ name: "origin_station", type: "varchar", length: 128 })
originStation!: string; originStation!: string;
@Column({ name: 'destination_station', type: 'varchar', length: 128 }) @Column({ name: "destination_station", type: "varchar", length: 128 })
destinationStation!: string; destinationStation!: string;
} }

View File

@@ -1,30 +1,37 @@
import { Body, Controller, Get, Param, ParseUUIDPipe, Post } from '@nestjs/common'; import {
import { ApiOperation, ApiTags } from '@nestjs/swagger'; Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { CustomersService } from './customers.service'; import { CustomersService } from "./customers.service";
import { CreateCustomerDto } from './dto/create-customer.dto'; import { CreateCustomerDto } from "./dto/create-customer.dto";
@ApiTags('customers') @ApiTags("customers")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth // @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller('customers') @Controller("customers")
export class CustomersController { export class CustomersController {
constructor(private readonly customersService: CustomersService) {} constructor(private readonly customersService: CustomersService) {}
@Post() @Post()
@ApiOperation({ summary: 'Create a new customer' }) @ApiOperation({ summary: "Create a new customer" })
create(@Body() dto: CreateCustomerDto) { create(@Body() dto: CreateCustomerDto) {
return this.customersService.create(dto); return this.customersService.create(dto);
} }
@Get() @Get()
@ApiOperation({ summary: 'List all customers' }) @ApiOperation({ summary: "List all customers" })
findAll() { findAll() {
return this.customersService.findAll(); return this.customersService.findAll();
} }
@Get(':id') @Get(":id")
@ApiOperation({ summary: 'Get a customer by ID' }) @ApiOperation({ summary: "Get a customer by ID" })
findOne(@Param('id', ParseUUIDPipe) id: string) { findOne(@Param("id", ParseUUIDPipe) id: string) {
return this.customersService.findById(id); return this.customersService.findById(id);
} }
} }

View File

@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { CustomersController } from './customers.controller'; import { CustomersController } from "./customers.controller";
import { CustomersRepository } from './customers.repository'; import { CustomersRepository } from "./customers.repository";
import { CustomersService } from './customers.service'; import { CustomersService } from "./customers.service";
import { Customer } from './entities/customer.entity'; import { Customer } from "./entities/customer.entity";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Customer])], imports: [TypeOrmModule.forFeature([Customer])],

View File

@@ -1,9 +1,9 @@
import { BaseRepository } from '@edr/api-common'; import { BaseRepository } from "@edr/api-common";
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { Customer } from './entities/customer.entity'; import { Customer } from "./entities/customer.entity";
@Injectable() @Injectable()
export class CustomersRepository extends BaseRepository<Customer> { export class CustomersRepository extends BaseRepository<Customer> {

View File

@@ -1,8 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from "@nestjs/common";
import { CustomersRepository } from './customers.repository'; import { CustomersRepository } from "./customers.repository";
import { CreateCustomerDto } from './dto/create-customer.dto'; import { CreateCustomerDto } from "./dto/create-customer.dto";
import { Customer } from './entities/customer.entity'; import { Customer } from "./entities/customer.entity";
@Injectable() @Injectable()
export class CustomersService { export class CustomersService {
@@ -15,7 +15,7 @@ export class CustomersService {
/** List every customer (alphabetical). */ /** List every customer (alphabetical). */
findAll(): Promise<Customer[]> { findAll(): Promise<Customer[]> {
return this.customersRepository.findAll({ order: { name: 'ASC' } }); return this.customersRepository.findAll({ order: { name: "ASC" } });
} }
/** Get a single customer by ID. */ /** Get a single customer by ID. */

View File

@@ -1,4 +1,4 @@
import { IsEmail, IsOptional, IsString } from 'class-validator'; import { IsEmail, IsOptional, IsString } from "class-validator";
export class CreateCustomerDto { export class CreateCustomerDto {
@IsString() @IsString()

View File

@@ -1,20 +1,20 @@
import { BaseEntity } from '@edr/api-common'; import { BaseEntity } from "@edr/api-common";
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
@Entity({ name: 'customers' }) @Entity({ name: "customers" })
export class Customer extends BaseEntity { export class Customer extends BaseEntity {
@Column({ name: 'name', type: 'varchar', length: 256 }) @Column({ name: "name", type: "varchar", length: 256 })
name!: string; name!: string;
@Column({ name: 'email', type: 'varchar', length: 256, unique: true }) @Column({ name: "email", type: "varchar", length: 256, unique: true })
email!: string; email!: string;
@Column({ name: 'phone', type: 'varchar', length: 32 }) @Column({ name: "phone", type: "varchar", length: 32 })
phone!: string; phone!: string;
@Column({ name: 'address', type: 'text', nullable: true }) @Column({ name: "address", type: "text", nullable: true })
address?: string | null; address?: string | null;
@Column({ name: 'tax_id', type: 'varchar', length: 64, nullable: true }) @Column({ name: "tax_id", type: "varchar", length: 64, nullable: true })
taxId?: string | null; taxId?: string | null;
} }

View File

@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { NotificationsService } from './notifications.service'; import { NotificationsService } from "./notifications.service";
@Module({ @Module({
providers: [NotificationsService], providers: [NotificationsService],

View File

@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from "@nestjs/common";
@Injectable() @Injectable()
export class NotificationsService { export class NotificationsService {

View File

@@ -1,21 +1,21 @@
import { BaseEntity } from '@edr/api-common'; import { BaseEntity } from "@edr/api-common";
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
@Entity({ name: 'tracking_events' }) @Entity({ name: "tracking_events" })
export class TrackingEvent extends BaseEntity { export class TrackingEvent extends BaseEntity {
@Column({ name: 'consignment_id', type: 'uuid' }) @Column({ name: "consignment_id", type: "uuid" })
consignmentId!: string; consignmentId!: string;
@Column({ name: 'location', type: 'varchar', length: 256 }) @Column({ name: "location", type: "varchar", length: 256 })
location!: string; location!: string;
@Column({ name: 'status', type: 'enum', enum: Freight.ConsignmentStatus }) @Column({ name: "status", type: "enum", enum: Freight.ConsignmentStatus })
status!: Freight.ConsignmentStatus; status!: Freight.ConsignmentStatus;
@Column({ name: 'occurred_at', type: 'timestamptz' }) @Column({ name: "occurred_at", type: "timestamptz" })
occurredAt!: Date; occurredAt!: Date;
@Column({ name: 'description', type: 'text', nullable: true }) @Column({ name: "description", type: "text", nullable: true })
description?: string | null; description?: string | null;
} }

View File

@@ -1,17 +1,19 @@
import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common'; import { Controller, Get, Param, ParseUUIDPipe } from "@nestjs/common";
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { TrackingService } from './tracking.service'; import { TrackingService } from "./tracking.service";
@ApiTags('tracking') @ApiTags("tracking")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth // @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller('tracking') @Controller("tracking")
export class TrackingController { export class TrackingController {
constructor(private readonly trackingService: TrackingService) {} constructor(private readonly trackingService: TrackingService) {}
@Get(':consignmentId') @Get(":consignmentId")
@ApiOperation({ summary: 'Get the tracking timeline for a consignment' }) @ApiOperation({ summary: "Get the tracking timeline for a consignment" })
findByConsignment(@Param('consignmentId', ParseUUIDPipe) consignmentId: string) { findByConsignment(
@Param("consignmentId", ParseUUIDPipe) consignmentId: string,
) {
return this.trackingService.findByConsignment(consignmentId); return this.trackingService.findByConsignment(consignmentId);
} }
} }

View File

@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { TrackingEvent } from './entities/tracking-event.entity'; import { TrackingEvent } from "./entities/tracking-event.entity";
import { TrackingController } from './tracking.controller'; import { TrackingController } from "./tracking.controller";
import { TrackingService } from './tracking.service'; import { TrackingService } from "./tracking.service";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([TrackingEvent])], imports: [TypeOrmModule.forFeature([TrackingEvent])],

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { TrackingEvent } from './entities/tracking-event.entity'; import { TrackingEvent } from "./entities/tracking-event.entity";
@Injectable() @Injectable()
export class TrackingService { export class TrackingService {
@@ -15,7 +15,7 @@ export class TrackingService {
findByConsignment(consignmentId: string): Promise<TrackingEvent[]> { findByConsignment(consignmentId: string): Promise<TrackingEvent[]> {
return this.trackingRepository.find({ return this.trackingRepository.find({
where: { consignmentId }, where: { consignmentId },
order: { occurredAt: 'ASC' }, order: { occurredAt: "ASC" },
}); });
} }

View File

@@ -1,5 +1,5 @@
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { IsEnum, IsNumber, IsOptional, IsString, Min } from 'class-validator'; import { IsEnum, IsNumber, IsOptional, IsString, Min } from "class-validator";
export class CreateTrainDto { export class CreateTrainDto {
@IsString() @IsString()

View File

@@ -1,23 +1,23 @@
import { BaseEntity } from '@edr/api-common'; import { BaseEntity } from "@edr/api-common";
import { Freight } from '@edr/types'; import { Freight } from "@edr/types";
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
@Entity({ name: 'trains' }) @Entity({ name: "trains" })
export class Train extends BaseEntity { export class Train extends BaseEntity {
@Column({ name: 'code', type: 'varchar', length: 32, unique: true }) @Column({ name: "code", type: "varchar", length: 32, unique: true })
code!: string; code!: string;
@Column({ name: 'capacity_tons', type: 'numeric', precision: 10, scale: 2 }) @Column({ name: "capacity_tons", type: "numeric", precision: 10, scale: 2 })
capacityTons!: number; capacityTons!: number;
@Column({ @Column({
name: 'status', name: "status",
type: 'enum', type: "enum",
enum: Freight.TrainStatus, enum: Freight.TrainStatus,
default: Freight.TrainStatus.Available, default: Freight.TrainStatus.Available,
}) })
status!: Freight.TrainStatus; status!: Freight.TrainStatus;
@Column({ name: 'notes', type: 'text', nullable: true }) @Column({ name: "notes", type: "text", nullable: true })
notes?: string | null; notes?: string | null;
} }

View File

@@ -1,30 +1,37 @@
import { Body, Controller, Get, Param, ParseUUIDPipe, Post } from '@nestjs/common'; import {
import { ApiOperation, ApiTags } from '@nestjs/swagger'; Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { CreateTrainDto } from './dto/create-train.dto'; import { CreateTrainDto } from "./dto/create-train.dto";
import { TrainsService } from './trains.service'; import { TrainsService } from "./trains.service";
@ApiTags('trains') @ApiTags("trains")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth // @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller('trains') @Controller("trains")
export class TrainsController { export class TrainsController {
constructor(private readonly trainsService: TrainsService) {} constructor(private readonly trainsService: TrainsService) {}
@Post() @Post()
@ApiOperation({ summary: 'Register a new train' }) @ApiOperation({ summary: "Register a new train" })
create(@Body() dto: CreateTrainDto) { create(@Body() dto: CreateTrainDto) {
return this.trainsService.create(dto); return this.trainsService.create(dto);
} }
@Get() @Get()
@ApiOperation({ summary: 'List all trains' }) @ApiOperation({ summary: "List all trains" })
findAll() { findAll() {
return this.trainsService.findAll(); return this.trainsService.findAll();
} }
@Get(':id') @Get(":id")
@ApiOperation({ summary: 'Get a train by ID' }) @ApiOperation({ summary: "Get a train by ID" })
findOne(@Param('id', ParseUUIDPipe) id: string) { findOne(@Param("id", ParseUUIDPipe) id: string) {
return this.trainsService.findById(id); return this.trainsService.findById(id);
} }
} }

View File

@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { Train } from './entities/train.entity'; import { Train } from "./entities/train.entity";
import { TrainsController } from './trains.controller'; import { TrainsController } from "./trains.controller";
import { TrainsRepository } from './trains.repository'; import { TrainsRepository } from "./trains.repository";
import { TrainsService } from './trains.service'; import { TrainsService } from "./trains.service";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Train])], imports: [TypeOrmModule.forFeature([Train])],

View File

@@ -1,9 +1,9 @@
import { BaseRepository } from '@edr/api-common'; import { BaseRepository } from "@edr/api-common";
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { Train } from './entities/train.entity'; import { Train } from "./entities/train.entity";
@Injectable() @Injectable()
export class TrainsRepository extends BaseRepository<Train> { export class TrainsRepository extends BaseRepository<Train> {

View File

@@ -1,8 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from "@nestjs/common";
import { CreateTrainDto } from './dto/create-train.dto'; import { CreateTrainDto } from "./dto/create-train.dto";
import { Train } from './entities/train.entity'; import { Train } from "./entities/train.entity";
import { TrainsRepository } from './trains.repository'; import { TrainsRepository } from "./trains.repository";
@Injectable() @Injectable()
export class TrainsService { export class TrainsService {
@@ -15,7 +15,7 @@ export class TrainsService {
/** List every active train. */ /** List every active train. */
findAll(): Promise<Train[]> { findAll(): Promise<Train[]> {
return this.trainsRepository.findAll({ order: { code: 'ASC' } }); return this.trainsRepository.findAll({ order: { code: "ASC" } });
} }
/** Get a single train by ID. */ /** Get a single train by ID. */

View File

@@ -1,10 +1,10 @@
import { INestApplication } from '@nestjs/common'; import { INestApplication } from "@nestjs/common";
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from "@nestjs/testing";
import request from 'supertest'; import request from "supertest";
import { AppModule } from '../src/app.module'; import { AppModule } from "../src/app.module";
describe('Freight API (e2e)', () => { describe("Freight API (e2e)", () => {
let app: INestApplication; let app: INestApplication;
beforeAll(async () => { beforeAll(async () => {
@@ -20,7 +20,7 @@ describe('Freight API (e2e)', () => {
await app.close(); await app.close();
}); });
it('GET /api/bookings returns a 200 with a list', () => { it("GET /api/bookings returns a 200 with a list", () => {
return request(app.getHttpServer()).get('/api/bookings').expect(200); return request(app.getHttpServer()).get("/api/bookings").expect(200);
}); });
}); });

View File

@@ -1,11 +1,15 @@
import { useNavigate, useLocation, Routes, Route, Navigate } from 'react-router-dom'; import {
import { DashboardLayout, type SidebarItem } from '@edr/ui-common'; useNavigate,
useLocation,
Routes,
Route,
Navigate,
} from "react-router-dom";
import { DashboardLayout, type SidebarItem } from "@edr/ui-common";
import DashboardPage from './pages/dashboard/DashboardPage'; import DashboardPage from "./pages/dashboard/DashboardPage";
const sidebarItems: SidebarItem[] = [ const sidebarItems: SidebarItem[] = [{ label: "Dashboard", href: "/" }];
{ label: 'Dashboard', href: '/' },
];
const App = () => { const App = () => {
const navigate = useNavigate(); const navigate = useNavigate();

View File

@@ -1,13 +1,13 @@
import { StrictMode } from 'react'; import { StrictMode } from "react";
import { createRoot } from 'react-dom/client'; import { createRoot } from "react-dom/client";
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from './App'; import App from "./App";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>

View File

@@ -1,14 +1,14 @@
import { defineConfig } from 'vite'; import { defineConfig } from "vite";
import react from '@vitejs/plugin-react'; import react from "@vitejs/plugin-react";
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
port: 5183, port: 5183,
host: '0.0.0.0', host: "0.0.0.0",
}, },
test: { test: {
environment: 'jsdom', environment: "jsdom",
globals: true, globals: true,
}, },
}); });

View File

@@ -1,23 +1,29 @@
import { useNavigate, useLocation, Routes, Route, Navigate } from 'react-router-dom'; import {
import { DashboardLayout, type SidebarItem } from '@edr/ui-common'; useNavigate,
useLocation,
Routes,
Route,
Navigate,
} from "react-router-dom";
import { DashboardLayout, type SidebarItem } from "@edr/ui-common";
import BookingsPage from './pages/bookings/BookingsPage'; import BookingsPage from "./pages/bookings/BookingsPage";
import BookingDetailPage from './pages/bookings/BookingDetailPage'; import BookingDetailPage from "./pages/bookings/BookingDetailPage";
import CreateBookingPage from './pages/bookings/CreateBookingPage'; import CreateBookingPage from "./pages/bookings/CreateBookingPage";
import ConsignmentsPage from './pages/consignments/ConsignmentsPage'; import ConsignmentsPage from "./pages/consignments/ConsignmentsPage";
import ConsignmentDetailPage from './pages/consignments/ConsignmentDetailPage'; import ConsignmentDetailPage from "./pages/consignments/ConsignmentDetailPage";
import TrackingPage from './pages/tracking/TrackingPage'; import TrackingPage from "./pages/tracking/TrackingPage";
import BillingPage from './pages/billing/BillingPage'; import BillingPage from "./pages/billing/BillingPage";
import TrainsPage from './pages/trains/TrainsPage'; import TrainsPage from "./pages/trains/TrainsPage";
import DashboardPage from './pages/dashboard/DashboardPage'; import DashboardPage from "./pages/dashboard/DashboardPage";
const sidebarItems: SidebarItem[] = [ const sidebarItems: SidebarItem[] = [
{ label: 'Dashboard', href: '/' }, { label: "Dashboard", href: "/" },
{ label: 'Bookings', href: '/bookings' }, { label: "Bookings", href: "/bookings" },
{ label: 'Consignments', href: '/consignments' }, { label: "Consignments", href: "/consignments" },
{ label: 'Tracking', href: '/tracking' }, { label: "Tracking", href: "/tracking" },
{ label: 'Trains', href: '/trains' }, { label: "Trains", href: "/trains" },
{ label: 'Billing', href: '/billing' }, { label: "Billing", href: "/billing" },
]; ];
const App = () => { const App = () => {

View File

@@ -1,7 +1,7 @@
import { FormEvent, useState } from 'react'; import { FormEvent, useState } from "react";
import { Button, FormField } from '@edr/ui-common'; import { Button, FormField } from "@edr/ui-common";
import type { CreateBookingPayload } from '../../services/bookings.service'; import type { CreateBookingPayload } from "../../services/bookings.service";
export interface BookingFormProps { export interface BookingFormProps {
onSubmit: (payload: CreateBookingPayload) => void; onSubmit: (payload: CreateBookingPayload) => void;
@@ -9,10 +9,10 @@ export interface BookingFormProps {
} }
const BookingForm = ({ onSubmit, isSubmitting }: BookingFormProps) => { const BookingForm = ({ onSubmit, isSubmitting }: BookingFormProps) => {
const [reference, setReference] = useState(''); const [reference, setReference] = useState("");
const [customerId, setCustomerId] = useState(''); const [customerId, setCustomerId] = useState("");
const [scheduledDate, setScheduledDate] = useState(''); const [scheduledDate, setScheduledDate] = useState("");
const [totalAmount, setTotalAmount] = useState('0'); const [totalAmount, setTotalAmount] = useState("0");
const handleSubmit = (event: FormEvent<HTMLFormElement>) => { const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
@@ -26,8 +26,18 @@ const BookingForm = ({ onSubmit, isSubmitting }: BookingFormProps) => {
return ( return (
<form onSubmit={handleSubmit} className="flex flex-col gap-3"> <form onSubmit={handleSubmit} className="flex flex-col gap-3">
<FormField label="Reference" value={reference} onChange={(e) => setReference(e.target.value)} required /> <FormField
<FormField label="Customer ID" value={customerId} onChange={(e) => setCustomerId(e.target.value)} required /> 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 <FormField
label="Scheduled date" label="Scheduled date"
type="date" type="date"

View File

@@ -1,28 +1,33 @@
import type { Freight } from '@edr/types'; import type { Freight } from "@edr/types";
import { Table, type TableColumn } from '@edr/ui-common'; import { Table, type TableColumn } from "@edr/ui-common";
export interface BookingTableProps { export interface BookingTableProps {
bookings: Freight.IBooking[]; bookings: Freight.IBooking[];
} }
const columns: TableColumn<Freight.IBooking>[] = [ const columns: TableColumn<Freight.IBooking>[] = [
{ key: 'reference', header: 'Reference' }, { key: "reference", header: "Reference" },
{ key: 'customerId', header: 'Customer' }, { key: "customerId", header: "Customer" },
{ key: 'status', header: 'Status' }, { key: "status", header: "Status" },
{ {
key: 'scheduledDate', key: "scheduledDate",
header: 'Scheduled', header: "Scheduled",
render: (row) => new Date(row.scheduledDate).toLocaleDateString(), render: (row) => new Date(row.scheduledDate).toLocaleDateString(),
}, },
{ {
key: 'totalAmount', key: "totalAmount",
header: 'Total', header: "Total",
render: (row) => row.totalAmount.toFixed(2), render: (row) => row.totalAmount.toFixed(2),
}, },
]; ];
const BookingTable = ({ bookings }: BookingTableProps) => ( const BookingTable = ({ bookings }: BookingTableProps) => (
<Table columns={columns} data={bookings} rowKey={(row) => row.id} emptyMessage="No bookings yet" /> <Table
columns={columns}
data={bookings}
rowKey={(row) => row.id}
emptyMessage="No bookings yet"
/>
); );
export default BookingTable; export default BookingTable;

View File

@@ -1,5 +1,5 @@
import { FormEvent, useState } from 'react'; import { FormEvent, useState } from "react";
import { Button, FormField } from '@edr/ui-common'; import { Button, FormField } from "@edr/ui-common";
export interface ConsignmentFormProps { export interface ConsignmentFormProps {
onSubmit: (payload: { onSubmit: (payload: {
@@ -14,12 +14,12 @@ export interface ConsignmentFormProps {
} }
const ConsignmentForm = ({ onSubmit, isSubmitting }: ConsignmentFormProps) => { const ConsignmentForm = ({ onSubmit, isSubmitting }: ConsignmentFormProps) => {
const [bookingId, setBookingId] = useState(''); const [bookingId, setBookingId] = useState("");
const [trackingNumber, setTrackingNumber] = useState(''); const [trackingNumber, setTrackingNumber] = useState("");
const [cargoType, setCargoType] = useState('GENERAL'); const [cargoType, setCargoType] = useState("GENERAL");
const [weightKg, setWeightKg] = useState('0'); const [weightKg, setWeightKg] = useState("0");
const [originStation, setOriginStation] = useState(''); const [originStation, setOriginStation] = useState("");
const [destinationStation, setDestinationStation] = useState(''); const [destinationStation, setDestinationStation] = useState("");
const handleSubmit = (event: FormEvent<HTMLFormElement>) => { const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
@@ -35,14 +35,23 @@ const ConsignmentForm = ({ onSubmit, isSubmitting }: ConsignmentFormProps) => {
return ( return (
<form onSubmit={handleSubmit} className="flex flex-col gap-3"> <form onSubmit={handleSubmit} className="flex flex-col gap-3">
<FormField label="Booking ID" value={bookingId} onChange={(e) => setBookingId(e.target.value)} required /> <FormField
label="Booking ID"
value={bookingId}
onChange={(e) => setBookingId(e.target.value)}
required
/>
<FormField <FormField
label="Tracking #" label="Tracking #"
value={trackingNumber} value={trackingNumber}
onChange={(e) => setTrackingNumber(e.target.value)} onChange={(e) => setTrackingNumber(e.target.value)}
required required
/> />
<FormField label="Cargo type" value={cargoType} onChange={(e) => setCargoType(e.target.value)} /> <FormField
label="Cargo type"
value={cargoType}
onChange={(e) => setCargoType(e.target.value)}
/>
<FormField <FormField
label="Weight (kg)" label="Weight (kg)"
type="number" type="number"

View File

@@ -1,17 +1,21 @@
import type { Freight } from '@edr/types'; import type { Freight } from "@edr/types";
import { Table, type TableColumn } from '@edr/ui-common'; import { Table, type TableColumn } from "@edr/ui-common";
export interface ConsignmentTableProps { export interface ConsignmentTableProps {
consignments: Freight.IConsignment[]; consignments: Freight.IConsignment[];
} }
const columns: TableColumn<Freight.IConsignment>[] = [ const columns: TableColumn<Freight.IConsignment>[] = [
{ key: 'trackingNumber', header: 'Tracking #' }, { key: "trackingNumber", header: "Tracking #" },
{ key: 'cargoType', header: 'Cargo' }, { key: "cargoType", header: "Cargo" },
{ key: 'status', header: 'Status' }, { key: "status", header: "Status" },
{ key: 'originStation', header: 'Origin' }, { key: "originStation", header: "Origin" },
{ key: 'destinationStation', header: 'Destination' }, { key: "destinationStation", header: "Destination" },
{ key: 'weightKg', header: 'Weight (kg)', render: (row) => row.weightKg.toFixed(2) }, {
key: "weightKg",
header: "Weight (kg)",
render: (row) => row.weightKg.toFixed(2),
},
]; ];
const ConsignmentTable = ({ consignments }: ConsignmentTableProps) => ( const ConsignmentTable = ({ consignments }: ConsignmentTableProps) => (

View File

@@ -1,5 +1,5 @@
import type { Freight } from '@edr/types'; import type { Freight } from "@edr/types";
import { Badge } from '@edr/ui-common'; import { Badge } from "@edr/ui-common";
export interface TrackingTimelineProps { export interface TrackingTimelineProps {
events: Freight.ITrackingEvent[]; events: Freight.ITrackingEvent[];

View File

@@ -1,16 +1,16 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from "@tanstack/react-query";
import { bookingsService } from '../services/bookings.service'; import { bookingsService } from "../services/bookings.service";
export const useBookings = () => export const useBookings = () =>
useQuery({ useQuery({
queryKey: ['bookings'], queryKey: ["bookings"],
queryFn: bookingsService.list, queryFn: bookingsService.list,
}); });
export const useBooking = (id: string) => export const useBooking = (id: string) =>
useQuery({ useQuery({
queryKey: ['bookings', id], queryKey: ["bookings", id],
queryFn: () => bookingsService.get(id), queryFn: () => bookingsService.get(id),
enabled: Boolean(id), enabled: Boolean(id),
}); });

View File

@@ -1,16 +1,16 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from "@tanstack/react-query";
import { consignmentsService } from '../services/consignments.service'; import { consignmentsService } from "../services/consignments.service";
export const useConsignments = () => export const useConsignments = () =>
useQuery({ useQuery({
queryKey: ['consignments'], queryKey: ["consignments"],
queryFn: consignmentsService.list, queryFn: consignmentsService.list,
}); });
export const useConsignment = (id: string) => export const useConsignment = (id: string) =>
useQuery({ useQuery({
queryKey: ['consignments', id], queryKey: ["consignments", id],
queryFn: () => consignmentsService.get(id), queryFn: () => consignmentsService.get(id),
enabled: Boolean(id), enabled: Boolean(id),
}); });

View File

@@ -1,10 +1,10 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from "@tanstack/react-query";
import { trackingService } from '../services/tracking.service'; import { trackingService } from "../services/tracking.service";
export const useTracking = (consignmentId: string) => export const useTracking = (consignmentId: string) =>
useQuery({ useQuery({
queryKey: ['tracking', consignmentId], queryKey: ["tracking", consignmentId],
queryFn: () => trackingService.forConsignment(consignmentId), queryFn: () => trackingService.forConsignment(consignmentId),
enabled: Boolean(consignmentId), enabled: Boolean(consignmentId),
}); });

View File

@@ -1,13 +1,13 @@
import { StrictMode } from 'react'; import { StrictMode } from "react";
import { createRoot } from 'react-dom/client'; import { createRoot } from "react-dom/client";
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from './App'; import App from "./App";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>

View File

@@ -2,8 +2,9 @@ const BillingPage = () => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h1 className="text-2xl font-semibold text-gray-900">Billing</h1> <h1 className="text-2xl font-semibold text-gray-900">Billing</h1>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
Invoice list and payment status will live here. Wire up @tanstack/react-query to{' '} Invoice list and payment status will live here. Wire up
<code>/billing/invoices</code> when the feature is built out. @tanstack/react-query to <code>/billing/invoices</code> when the feature
is built out.
</p> </p>
</div> </div>
); );

View File

@@ -1,24 +1,29 @@
import { useParams } from 'react-router-dom'; import { useParams } from "react-router-dom";
import { useBooking } from '../../hooks/useBookings'; import { useBooking } from "../../hooks/useBookings";
const BookingDetailPage = () => { const BookingDetailPage = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const { data: booking, isLoading } = useBooking(id ?? ''); const { data: booking, isLoading } = useBooking(id ?? "");
if (isLoading) return <div className="text-sm text-gray-500">Loading</div>; 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>; if (!booking)
return <div className="text-sm text-red-600">Booking not found.</div>;
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<h1 className="text-2xl font-semibold text-gray-900">Booking {booking.reference}</h1> <h1 className="text-2xl font-semibold text-gray-900">
Booking {booking.reference}
</h1>
<dl className="grid grid-cols-2 gap-2 text-sm"> <dl className="grid grid-cols-2 gap-2 text-sm">
<dt className="text-gray-500">Status</dt> <dt className="text-gray-500">Status</dt>
<dd className="text-gray-900">{booking.status}</dd> <dd className="text-gray-900">{booking.status}</dd>
<dt className="text-gray-500">Customer ID</dt> <dt className="text-gray-500">Customer ID</dt>
<dd className="text-gray-900">{booking.customerId}</dd> <dd className="text-gray-900">{booking.customerId}</dd>
<dt className="text-gray-500">Scheduled</dt> <dt className="text-gray-500">Scheduled</dt>
<dd className="text-gray-900">{new Date(booking.scheduledDate).toLocaleString()}</dd> <dd className="text-gray-900">
{new Date(booking.scheduledDate).toLocaleString()}
</dd>
<dt className="text-gray-500">Total amount</dt> <dt className="text-gray-500">Total amount</dt>
<dd className="text-gray-900">{booking.totalAmount.toFixed(2)}</dd> <dd className="text-gray-900">{booking.totalAmount.toFixed(2)}</dd>
</dl> </dl>

View File

@@ -1,8 +1,8 @@
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import { Button } from '@edr/ui-common'; import { Button } from "@edr/ui-common";
import BookingTable from '../../components/bookings/BookingTable'; import BookingTable from "../../components/bookings/BookingTable";
import { useBookings } from '../../hooks/useBookings'; import { useBookings } from "../../hooks/useBookings";
const BookingsPage = () => { const BookingsPage = () => {
const { data, isLoading } = useBookings(); const { data, isLoading } = useBookings();
@@ -16,7 +16,11 @@ const BookingsPage = () => {
<Button>New booking</Button> <Button>New booking</Button>
</Link> </Link>
</div> </div>
{isLoading ? <div className="text-sm text-gray-500">Loading</div> : <BookingTable bookings={items} />} {isLoading ? (
<div className="text-sm text-gray-500">Loading</div>
) : (
<BookingTable bookings={items} />
)}
</div> </div>
); );
}; };

View File

@@ -1,8 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import BookingForm from '../../components/bookings/BookingForm'; import BookingForm from "../../components/bookings/BookingForm";
import { bookingsService } from '../../services/bookings.service'; import { bookingsService } from "../../services/bookings.service";
const CreateBookingPage = () => { const CreateBookingPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -11,15 +11,18 @@ const CreateBookingPage = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationFn: bookingsService.create, mutationFn: bookingsService.create,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bookings'] }); queryClient.invalidateQueries({ queryKey: ["bookings"] });
navigate('/bookings'); navigate("/bookings");
}, },
}); });
return ( return (
<div className="max-w-lg"> <div className="max-w-lg">
<h1 className="mb-4 text-2xl font-semibold text-gray-900">New booking</h1> <h1 className="mb-4 text-2xl font-semibold text-gray-900">New booking</h1>
<BookingForm onSubmit={mutation.mutate} isSubmitting={mutation.isPending} /> <BookingForm
onSubmit={mutation.mutate}
isSubmitting={mutation.isPending}
/>
</div> </div>
); );
}; };

View File

@@ -1,17 +1,20 @@
import { useParams } from 'react-router-dom'; import { useParams } from "react-router-dom";
import { useConsignment } from '../../hooks/useConsignments'; import { useConsignment } from "../../hooks/useConsignments";
const ConsignmentDetailPage = () => { const ConsignmentDetailPage = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const { data, isLoading } = useConsignment(id ?? ''); const { data, isLoading } = useConsignment(id ?? "");
if (isLoading) return <div className="text-sm text-gray-500">Loading</div>; 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>; if (!data)
return <div className="text-sm text-red-600">Consignment not found.</div>;
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<h1 className="text-2xl font-semibold text-gray-900">Consignment {data.trackingNumber}</h1> <h1 className="text-2xl font-semibold text-gray-900">
Consignment {data.trackingNumber}
</h1>
<dl className="grid grid-cols-2 gap-2 text-sm"> <dl className="grid grid-cols-2 gap-2 text-sm">
<dt className="text-gray-500">Status</dt> <dt className="text-gray-500">Status</dt>
<dd className="text-gray-900">{data.status}</dd> <dd className="text-gray-900">{data.status}</dd>

View File

@@ -1,5 +1,5 @@
import ConsignmentTable from '../../components/consignments/ConsignmentTable'; import ConsignmentTable from "../../components/consignments/ConsignmentTable";
import { useConsignments } from '../../hooks/useConsignments'; import { useConsignments } from "../../hooks/useConsignments";
const ConsignmentsPage = () => { const ConsignmentsPage = () => {
const { data, isLoading } = useConsignments(); const { data, isLoading } = useConsignments();

View File

@@ -1,12 +1,12 @@
import { useState } from 'react'; import { useState } from "react";
import { Button, FormField } from '@edr/ui-common'; import { Button, FormField } from "@edr/ui-common";
import TrackingTimeline from '../../components/tracking/TrackingTimeline'; import TrackingTimeline from "../../components/tracking/TrackingTimeline";
import { useTracking } from '../../hooks/useTracking'; import { useTracking } from "../../hooks/useTracking";
const TrackingPage = () => { const TrackingPage = () => {
const [consignmentId, setConsignmentId] = useState(''); const [consignmentId, setConsignmentId] = useState("");
const [activeId, setActiveId] = useState(''); const [activeId, setActiveId] = useState("");
const { data, isFetching } = useTracking(activeId); const { data, isFetching } = useTracking(activeId);
return ( return (

View File

@@ -2,7 +2,8 @@ const TrainsPage = () => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h1 className="text-2xl font-semibold text-gray-900">Trains</h1> <h1 className="text-2xl font-semibold text-gray-900">Trains</h1>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
Fleet roster, capacity, and maintenance status. Connect to <code>/trains</code> when ready. Fleet roster, capacity, and maintenance status. Connect to{" "}
<code>/trains</code> when ready.
</p> </p>
</div> </div>
); );

View File

@@ -1,6 +1,6 @@
import type { Freight, PaginatedResponse } from '@edr/types'; import type { Freight, PaginatedResponse } from "@edr/types";
import { api } from '../utils/api'; import { api } from "../utils/api";
export interface CreateBookingPayload { export interface CreateBookingPayload {
reference: string; reference: string;
@@ -12,7 +12,7 @@ export interface CreateBookingPayload {
export const bookingsService = { export const bookingsService = {
list: async (): Promise<PaginatedResponse<Freight.IBooking>> => { list: async (): Promise<PaginatedResponse<Freight.IBooking>> => {
const { data } = await api.get('/bookings'); const { data } = await api.get("/bookings");
return data.data; return data.data;
}, },
get: async (id: string): Promise<Freight.IBooking> => { get: async (id: string): Promise<Freight.IBooking> => {
@@ -20,7 +20,7 @@ export const bookingsService = {
return data.data; return data.data;
}, },
create: async (payload: CreateBookingPayload): Promise<Freight.IBooking> => { create: async (payload: CreateBookingPayload): Promise<Freight.IBooking> => {
const { data } = await api.post('/bookings', payload); const { data } = await api.post("/bookings", payload);
return data.data; return data.data;
}, },
remove: async (id: string): Promise<void> => { remove: async (id: string): Promise<void> => {

View File

@@ -1,10 +1,10 @@
import type { Freight, PaginatedResponse } from '@edr/types'; import type { Freight, PaginatedResponse } from "@edr/types";
import { api } from '../utils/api'; import { api } from "../utils/api";
export const consignmentsService = { export const consignmentsService = {
list: async (): Promise<PaginatedResponse<Freight.IConsignment>> => { list: async (): Promise<PaginatedResponse<Freight.IConsignment>> => {
const { data } = await api.get('/consignments'); const { data } = await api.get("/consignments");
return data.data; return data.data;
}, },
get: async (id: string): Promise<Freight.IConsignment> => { get: async (id: string): Promise<Freight.IConsignment> => {

View File

@@ -1,9 +1,11 @@
import type { Freight } from '@edr/types'; import type { Freight } from "@edr/types";
import { api } from '../utils/api'; import { api } from "../utils/api";
export const trackingService = { export const trackingService = {
forConsignment: async (consignmentId: string): Promise<Freight.ITrackingEvent[]> => { forConsignment: async (
consignmentId: string,
): Promise<Freight.ITrackingEvent[]> => {
const { data } = await api.get(`/tracking/${consignmentId}`); const { data } = await api.get(`/tracking/${consignmentId}`);
return data.data; return data.data;
}, },

View File

@@ -1,4 +1,4 @@
import { create } from 'zustand'; import { create } from "zustand";
interface AppState { interface AppState {
// TODO: integrate @edr/auth — currentUser will be sourced from the auth package // TODO: integrate @edr/auth — currentUser will be sourced from the auth package
@@ -8,5 +8,6 @@ interface AppState {
export const useAppStore = create<AppState>((set) => ({ export const useAppStore = create<AppState>((set) => ({
isSidebarOpen: true, isSidebarOpen: true,
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })), toggleSidebar: () =>
set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
})); }));

View File

@@ -1,4 +1,4 @@
export type { Freight } from '@edr/types'; export type { Freight } from "@edr/types";
export interface NavItem { export interface NavItem {
href: string; href: string;

View File

@@ -1,4 +1,4 @@
import axios from 'axios'; import axios from "axios";
export const api = axios.create({ export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL, baseURL: import.meta.env.VITE_API_URL,

View File

@@ -1,14 +1,14 @@
import { defineConfig } from 'vite'; import { defineConfig } from "vite";
import react from '@vitejs/plugin-react'; import react from "@vitejs/plugin-react";
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
port: 5173, port: 5173,
host: '0.0.0.0', host: "0.0.0.0",
}, },
test: { test: {
environment: 'jsdom', environment: "jsdom",
globals: true, globals: true,
}, },
}); });

View File

@@ -51,11 +51,19 @@
"typescript": "^5.5.4" "typescript": "^5.5.4"
}, },
"jest": { "jest": {
"moduleFileExtensions": ["js", "json", "ts"], "moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src", "rootDir": "src",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "^.+\\.(t|j)s$": "ts-jest" }, "transform": {
"collectCoverageFrom": ["**/*.(t|j)s"], "^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
} }

View File

@@ -1,17 +1,17 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
import appConfig from './config/app.config'; import appConfig from "./config/app.config";
import databaseConfig from './config/database.config'; import databaseConfig from "./config/database.config";
import { TicketsModule } from './modules/tickets/tickets.module'; import { TicketsModule } from "./modules/tickets/tickets.module";
import { SchedulesModule } from './modules/schedules/schedules.module'; import { SchedulesModule } from "./modules/schedules/schedules.module";
import { PassengersModule } from './modules/passengers/passengers.module'; import { PassengersModule } from "./modules/passengers/passengers.module";
import { SeatsModule } from './modules/seats/seats.module'; import { SeatsModule } from "./modules/seats/seats.module";
import { StationsModule } from './modules/stations/stations.module'; import { StationsModule } from "./modules/stations/stations.module";
import { PaymentsModule } from './modules/payments/payments.module'; import { PaymentsModule } from "./modules/payments/payments.module";
import { NotificationsModule } from './modules/notifications/notifications.module'; import { NotificationsModule } from "./modules/notifications/notifications.module";
@Module({ @Module({
imports: [ imports: [
@@ -22,7 +22,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
inject: [ConfigService], inject: [ConfigService],
useFactory: (config: ConfigService): TypeOrmModuleOptions => useFactory: (config: ConfigService): TypeOrmModuleOptions =>
config.get<TypeOrmModuleOptions>('database')!, config.get<TypeOrmModuleOptions>("database")!,
}), }),
TicketsModule, TicketsModule,
SchedulesModule, SchedulesModule,

View File

@@ -1 +1 @@
export { HttpExceptionFilter } from '@edr/api-common'; export { HttpExceptionFilter } from "@edr/api-common";

View File

@@ -1 +1 @@
export { ResponseTransformInterceptor } from '@edr/api-common'; export { ResponseTransformInterceptor } from "@edr/api-common";

View File

@@ -1 +1 @@
export { createValidationPipe } from '@edr/api-common'; export { createValidationPipe } from "@edr/api-common";

View File

@@ -1,7 +1,7 @@
import { registerAs } from '@nestjs/config'; import { registerAs } from "@nestjs/config";
export default registerAs('app', () => ({ export default registerAs("app", () => ({
env: process.env.NODE_ENV ?? 'development', env: process.env.NODE_ENV ?? "development",
port: parseInt(process.env.PORT ?? '3002', 10), port: parseInt(process.env.PORT ?? "3002", 10),
apiPrefix: 'api', apiPrefix: "api",
})); }));

View File

@@ -1,15 +1,18 @@
import { registerAs } from '@nestjs/config'; import { registerAs } from "@nestjs/config";
import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { TypeOrmModuleOptions } from "@nestjs/typeorm";
export default registerAs('database', (): TypeOrmModuleOptions => ({ export default registerAs(
type: 'postgres', "database",
host: process.env.DB_HOST ?? 'localhost', (): TypeOrmModuleOptions => ({
port: parseInt(process.env.DB_PORT ?? '5434', 10), type: "postgres",
username: process.env.DB_USER ?? 'postgres', host: process.env.DB_HOST ?? "localhost",
password: process.env.DB_PASSWORD ?? '', port: parseInt(process.env.DB_PORT ?? "5434", 10),
database: process.env.DB_NAME ?? 'edr_passenger', username: process.env.DB_USER ?? "postgres",
entities: [__dirname + '/../**/*.entity.{ts,js}'], password: process.env.DB_PASSWORD ?? "",
migrations: [__dirname + '/../../migrations/*.{ts,js}'], database: process.env.DB_NAME ?? "edr_passenger",
synchronize: process.env.NODE_ENV === 'development', entities: [__dirname + "/../**/*.entity.{ts,js}"],
logging: process.env.NODE_ENV === 'development', migrations: [__dirname + "/../../migrations/*.{ts,js}"],
})); synchronize: process.env.NODE_ENV === "development",
logging: process.env.NODE_ENV === "development",
}),
);

View File

@@ -1,28 +1,32 @@
import 'reflect-metadata'; import "reflect-metadata";
import { NestFactory } from '@nestjs/core'; import { NestFactory } from "@nestjs/core";
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { HttpExceptionFilter, ResponseTransformInterceptor, createValidationPipe } from '@edr/api-common'; import {
HttpExceptionFilter,
ResponseTransformInterceptor,
createValidationPipe,
} from "@edr/api-common";
import { AppModule } from './app.module'; import { AppModule } from "./app.module";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true }); const app = await NestFactory.create(AppModule, { cors: true });
app.setGlobalPrefix('api'); app.setGlobalPrefix("api");
app.useGlobalPipes(createValidationPipe()); app.useGlobalPipes(createValidationPipe());
app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResponseTransformInterceptor()); app.useGlobalInterceptors(new ResponseTransformInterceptor());
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('EDR Passenger API') .setTitle("EDR Passenger API")
.setDescription('API for the EDR Passenger Management application') .setDescription("API for the EDR Passenger Management application")
.setVersion('0.1.0') .setVersion("0.1.0")
.addBearerAuth() .addBearerAuth()
.build(); .build();
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document); SwaggerModule.setup("api/docs", app, document);
const port = parseInt(process.env.PORT ?? '3002', 10); const port = parseInt(process.env.PORT ?? "3002", 10);
await app.listen(port); await app.listen(port);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`[passenger-api] listening on http://localhost:${port}`); console.log(`[passenger-api] listening on http://localhost:${port}`);

View File

@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { NotificationsService } from './notifications.service'; import { NotificationsService } from "./notifications.service";
@Module({ @Module({
providers: [NotificationsService], providers: [NotificationsService],

View File

@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from "@nestjs/common";
@Injectable() @Injectable()
export class NotificationsService { export class NotificationsService {

View File

@@ -1,4 +1,4 @@
import { IsDateString, IsEmail, IsOptional, IsString } from 'class-validator'; import { IsDateString, IsEmail, IsOptional, IsString } from "class-validator";
export class CreatePassengerDto { export class CreatePassengerDto {
@IsString() @IsString()

View File

@@ -1,20 +1,20 @@
import { BaseEntity } from '@edr/api-common'; import { BaseEntity } from "@edr/api-common";
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
@Entity({ name: 'passengers' }) @Entity({ name: "passengers" })
export class Passenger extends BaseEntity { export class Passenger extends BaseEntity {
@Column({ name: 'full_name', type: 'varchar', length: 256 }) @Column({ name: "full_name", type: "varchar", length: 256 })
fullName!: string; fullName!: string;
@Column({ name: 'email', type: 'varchar', length: 256, unique: true }) @Column({ name: "email", type: "varchar", length: 256, unique: true })
email!: string; email!: string;
@Column({ name: 'phone', type: 'varchar', length: 32 }) @Column({ name: "phone", type: "varchar", length: 32 })
phone!: string; phone!: string;
@Column({ name: 'national_id', type: 'varchar', length: 64, nullable: true }) @Column({ name: "national_id", type: "varchar", length: 64, nullable: true })
nationalId?: string | null; nationalId?: string | null;
@Column({ name: 'date_of_birth', type: 'date', nullable: true }) @Column({ name: "date_of_birth", type: "date", nullable: true })
dateOfBirth?: string | null; dateOfBirth?: string | null;
} }

View File

@@ -1,30 +1,37 @@
import { Body, Controller, Get, Param, ParseUUIDPipe, Post } from '@nestjs/common'; import {
import { ApiOperation, ApiTags } from '@nestjs/swagger'; Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { CreatePassengerDto } from './dto/create-passenger.dto'; import { CreatePassengerDto } from "./dto/create-passenger.dto";
import { PassengersService } from './passengers.service'; import { PassengersService } from "./passengers.service";
@ApiTags('passengers') @ApiTags("passengers")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth // @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller('passengers') @Controller("passengers")
export class PassengersController { export class PassengersController {
constructor(private readonly passengersService: PassengersService) {} constructor(private readonly passengersService: PassengersService) {}
@Post() @Post()
@ApiOperation({ summary: 'Register a new passenger' }) @ApiOperation({ summary: "Register a new passenger" })
create(@Body() dto: CreatePassengerDto) { create(@Body() dto: CreatePassengerDto) {
return this.passengersService.create(dto); return this.passengersService.create(dto);
} }
@Get() @Get()
@ApiOperation({ summary: 'List all passengers' }) @ApiOperation({ summary: "List all passengers" })
findAll() { findAll() {
return this.passengersService.findAll(); return this.passengersService.findAll();
} }
@Get(':id') @Get(":id")
@ApiOperation({ summary: 'Get a passenger by ID' }) @ApiOperation({ summary: "Get a passenger by ID" })
findOne(@Param('id', ParseUUIDPipe) id: string) { findOne(@Param("id", ParseUUIDPipe) id: string) {
return this.passengersService.findById(id); return this.passengersService.findById(id);
} }
} }

View File

@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { Passenger } from './entities/passenger.entity'; import { Passenger } from "./entities/passenger.entity";
import { PassengersController } from './passengers.controller'; import { PassengersController } from "./passengers.controller";
import { PassengersRepository } from './passengers.repository'; import { PassengersRepository } from "./passengers.repository";
import { PassengersService } from './passengers.service'; import { PassengersService } from "./passengers.service";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Passenger])], imports: [TypeOrmModule.forFeature([Passenger])],

View File

@@ -1,9 +1,9 @@
import { BaseRepository } from '@edr/api-common'; import { BaseRepository } from "@edr/api-common";
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { Passenger } from './entities/passenger.entity'; import { Passenger } from "./entities/passenger.entity";
@Injectable() @Injectable()
export class PassengersRepository extends BaseRepository<Passenger> { export class PassengersRepository extends BaseRepository<Passenger> {

View File

@@ -1,8 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from "@nestjs/common";
import { CreatePassengerDto } from './dto/create-passenger.dto'; import { CreatePassengerDto } from "./dto/create-passenger.dto";
import { Passenger } from './entities/passenger.entity'; import { Passenger } from "./entities/passenger.entity";
import { PassengersRepository } from './passengers.repository'; import { PassengersRepository } from "./passengers.repository";
@Injectable() @Injectable()
export class PassengersService { export class PassengersService {
@@ -15,7 +15,7 @@ export class PassengersService {
/** List every passenger (alphabetical). */ /** List every passenger (alphabetical). */
findAll(): Promise<Passenger[]> { findAll(): Promise<Passenger[]> {
return this.passengersRepository.findAll({ order: { fullName: 'ASC' } }); return this.passengersRepository.findAll({ order: { fullName: "ASC" } });
} }
/** Get a single passenger by ID. */ /** Get a single passenger by ID. */

View File

@@ -1,32 +1,37 @@
import { BaseEntity } from '@edr/api-common'; import { BaseEntity } from "@edr/api-common";
import { Passenger } from '@edr/types'; import { Passenger } from "@edr/types";
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
@Entity({ name: 'payments' }) @Entity({ name: "payments" })
export class Payment extends BaseEntity { export class Payment extends BaseEntity {
@Column({ name: 'ticket_id', type: 'uuid' }) @Column({ name: "ticket_id", type: "uuid" })
ticketId!: string; ticketId!: string;
@Column({ name: 'amount', type: 'numeric', precision: 10, scale: 2 }) @Column({ name: "amount", type: "numeric", precision: 10, scale: 2 })
amount!: number; amount!: number;
@Column({ name: 'currency', type: 'varchar', length: 8, default: 'ETB' }) @Column({ name: "currency", type: "varchar", length: 8, default: "ETB" })
currency!: string; currency!: string;
@Column({ @Column({
name: 'status', name: "status",
type: 'enum', type: "enum",
enum: Passenger.PaymentStatus, enum: Passenger.PaymentStatus,
default: Passenger.PaymentStatus.Pending, default: Passenger.PaymentStatus.Pending,
}) })
status!: Passenger.PaymentStatus; status!: Passenger.PaymentStatus;
@Column({ name: 'provider', type: 'varchar', length: 64 }) @Column({ name: "provider", type: "varchar", length: 64 })
provider!: string; provider!: string;
@Column({ name: 'provider_transaction_id', type: 'varchar', length: 256, nullable: true }) @Column({
name: "provider_transaction_id",
type: "varchar",
length: 256,
nullable: true,
})
providerTransactionId?: string | null; providerTransactionId?: string | null;
@Column({ name: 'paid_at', type: 'timestamptz', nullable: true }) @Column({ name: "paid_at", type: "timestamptz", nullable: true })
paidAt?: Date | null; paidAt?: Date | null;
} }

View File

@@ -1,17 +1,17 @@
import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common'; import { Controller, Get, Param, ParseUUIDPipe } from "@nestjs/common";
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { PaymentsService } from './payments.service'; import { PaymentsService } from "./payments.service";
@ApiTags('payments') @ApiTags("payments")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth // @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller('payments') @Controller("payments")
export class PaymentsController { export class PaymentsController {
constructor(private readonly paymentsService: PaymentsService) {} constructor(private readonly paymentsService: PaymentsService) {}
@Get('ticket/:ticketId') @Get("ticket/:ticketId")
@ApiOperation({ summary: 'List payments for a ticket' }) @ApiOperation({ summary: "List payments for a ticket" })
findByTicket(@Param('ticketId', ParseUUIDPipe) ticketId: string) { findByTicket(@Param("ticketId", ParseUUIDPipe) ticketId: string) {
return this.paymentsService.findByTicket(ticketId); return this.paymentsService.findByTicket(ticketId);
} }
} }

View File

@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { Payment } from './entities/payment.entity'; import { Payment } from "./entities/payment.entity";
import { PaymentsController } from './payments.controller'; import { PaymentsController } from "./payments.controller";
import { PaymentsService } from './payments.service'; import { PaymentsService } from "./payments.service";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Payment])], imports: [TypeOrmModule.forFeature([Payment])],

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { Payment } from './entities/payment.entity'; import { Payment } from "./entities/payment.entity";
@Injectable() @Injectable()
export class PaymentsService { export class PaymentsService {
@@ -15,7 +15,7 @@ export class PaymentsService {
findByTicket(ticketId: string): Promise<Payment[]> { findByTicket(ticketId: string): Promise<Payment[]> {
return this.paymentsRepository.find({ return this.paymentsRepository.find({
where: { ticketId }, where: { ticketId },
order: { createdAt: 'DESC' }, order: { createdAt: "DESC" },
}); });
} }
} }

View File

@@ -1,5 +1,13 @@
import { Passenger } from '@edr/types'; import { Passenger } from "@edr/types";
import { IsDateString, IsEnum, IsNumber, IsOptional, IsString, IsUUID, Min } from 'class-validator'; import {
IsDateString,
IsEnum,
IsNumber,
IsOptional,
IsString,
IsUUID,
Min,
} from "class-validator";
export class CreateScheduleDto { export class CreateScheduleDto {
@IsString() @IsString()

View File

@@ -1,32 +1,32 @@
import { BaseEntity } from '@edr/api-common'; import { BaseEntity } from "@edr/api-common";
import { Passenger } from '@edr/types'; import { Passenger } from "@edr/types";
import { Column, Entity } from 'typeorm'; import { Column, Entity } from "typeorm";
@Entity({ name: 'schedules' }) @Entity({ name: "schedules" })
export class Schedule extends BaseEntity { export class Schedule extends BaseEntity {
@Column({ name: 'train_code', type: 'varchar', length: 32 }) @Column({ name: "train_code", type: "varchar", length: 32 })
trainCode!: string; trainCode!: string;
@Column({ name: 'origin_station_id', type: 'uuid' }) @Column({ name: "origin_station_id", type: "uuid" })
originStationId!: string; originStationId!: string;
@Column({ name: 'destination_station_id', type: 'uuid' }) @Column({ name: "destination_station_id", type: "uuid" })
destinationStationId!: string; destinationStationId!: string;
@Column({ name: 'departure_time', type: 'timestamptz' }) @Column({ name: "departure_time", type: "timestamptz" })
departureTime!: Date; departureTime!: Date;
@Column({ name: 'arrival_time', type: 'timestamptz' }) @Column({ name: "arrival_time", type: "timestamptz" })
arrivalTime!: Date; arrivalTime!: Date;
@Column({ @Column({
name: 'status', name: "status",
type: 'enum', type: "enum",
enum: Passenger.ScheduleStatus, enum: Passenger.ScheduleStatus,
default: Passenger.ScheduleStatus.Scheduled, default: Passenger.ScheduleStatus.Scheduled,
}) })
status!: Passenger.ScheduleStatus; status!: Passenger.ScheduleStatus;
@Column({ name: 'base_price', type: 'numeric', precision: 10, scale: 2 }) @Column({ name: "base_price", type: "numeric", precision: 10, scale: 2 })
basePrice!: number; basePrice!: number;
} }

View File

@@ -1,30 +1,37 @@
import { Body, Controller, Get, Param, ParseUUIDPipe, Post } from '@nestjs/common'; import {
import { ApiOperation, ApiTags } from '@nestjs/swagger'; Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { CreateScheduleDto } from './dto/create-schedule.dto'; import { CreateScheduleDto } from "./dto/create-schedule.dto";
import { SchedulesService } from './schedules.service'; import { SchedulesService } from "./schedules.service";
@ApiTags('schedules') @ApiTags("schedules")
// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth // @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth
@Controller('schedules') @Controller("schedules")
export class SchedulesController { export class SchedulesController {
constructor(private readonly schedulesService: SchedulesService) {} constructor(private readonly schedulesService: SchedulesService) {}
@Post() @Post()
@ApiOperation({ summary: 'Publish a new train schedule' }) @ApiOperation({ summary: "Publish a new train schedule" })
create(@Body() dto: CreateScheduleDto) { create(@Body() dto: CreateScheduleDto) {
return this.schedulesService.create(dto); return this.schedulesService.create(dto);
} }
@Get() @Get()
@ApiOperation({ summary: 'List all schedules' }) @ApiOperation({ summary: "List all schedules" })
findAll() { findAll() {
return this.schedulesService.findAll(); return this.schedulesService.findAll();
} }
@Get(':id') @Get(":id")
@ApiOperation({ summary: 'Get a schedule by ID' }) @ApiOperation({ summary: "Get a schedule by ID" })
findOne(@Param('id', ParseUUIDPipe) id: string) { findOne(@Param("id", ParseUUIDPipe) id: string) {
return this.schedulesService.findById(id); return this.schedulesService.findById(id);
} }
} }

View File

@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from "@nestjs/common";
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from "@nestjs/typeorm";
import { Schedule } from './entities/schedule.entity'; import { Schedule } from "./entities/schedule.entity";
import { SchedulesController } from './schedules.controller'; import { SchedulesController } from "./schedules.controller";
import { SchedulesRepository } from './schedules.repository'; import { SchedulesRepository } from "./schedules.repository";
import { SchedulesService } from './schedules.service'; import { SchedulesService } from "./schedules.service";
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Schedule])], imports: [TypeOrmModule.forFeature([Schedule])],

View File

@@ -1,9 +1,9 @@
import { BaseRepository } from '@edr/api-common'; import { BaseRepository } from "@edr/api-common";
import { Injectable } from '@nestjs/common'; import { Injectable } from "@nestjs/common";
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm'; import { Repository } from "typeorm";
import { Schedule } from './entities/schedule.entity'; import { Schedule } from "./entities/schedule.entity";
@Injectable() @Injectable()
export class SchedulesRepository extends BaseRepository<Schedule> { export class SchedulesRepository extends BaseRepository<Schedule> {

Some files were not shown because too many files have changed in this diff Show More