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
## 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.
## Apps
| 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-web/backoffice` | `@edr/freight-backoffice` | React frontend for freight backoffice employees | 5183 |
| `edr-passenger-api` | `@edr/passenger-api` | NestJS API for passenger management | 3002 |
| `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 |
| 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-web/backoffice` | `@edr/freight-backoffice` | React frontend for freight backoffice employees | 5183 |
| `edr-passenger-api` | `@edr/passenger-api` | NestJS API for passenger management | 3002 |
| `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.
## Packages
| Package | Purpose |
|---|---|
| `@edr/types` | Shared TypeScript interfaces and enums |
| `@edr/api-common` | Shared NestJS decorators, filters, interceptors, pipes, BaseEntity, BaseRepository |
| `@edr/ui-common` | Shared React components and theme |
| `@edr/eslint-config` | Shared ESLint configurations (base/nestjs/react) |
| `@edr/tsconfig` | Shared TypeScript configurations |
| `@edr/prettier-config` | Shared Prettier configuration |
| Package | Purpose |
| ---------------------- | ---------------------------------------------------------------------------------- |
| `@edr/types` | Shared TypeScript interfaces and enums |
| `@edr/api-common` | Shared NestJS decorators, filters, interceptors, pipes, BaseEntity, BaseRepository |
| `@edr/ui-common` | Shared React components and theme |
| `@edr/eslint-config` | Shared ESLint configurations (base/nestjs/react) |
| `@edr/tsconfig` | Shared TypeScript configurations |
| `@edr/prettier-config` | Shared Prettier configuration |
## Commands
| Command | Description |
|---|---|
| `pnpm install` | Install all workspace dependencies |
| `pnpm dev` | Run every app in dev mode |
| `pnpm dev:freight` | Run only freight API + web |
| `pnpm dev:passenger` | Run only passenger API + web |
| `pnpm build` | Build every package and app |
| `pnpm test` | Run all tests |
| `pnpm lint` | Lint everything |
| `pnpm type-check` | Type-check every package |
| `pnpm format` | Format all files with Prettier |
| Command | Description |
| -------------------- | ---------------------------------- |
| `pnpm install` | Install all workspace dependencies |
| `pnpm dev` | Run every app in dev mode |
| `pnpm dev:freight` | Run only freight API + web |
| `pnpm dev:passenger` | Run only passenger API + web |
| `pnpm build` | Build every package and app |
| `pnpm test` | Run all tests |
| `pnpm lint` | Lint everything |
| `pnpm type-check` | Type-check every package |
| `pnpm format` | Format all files with Prettier |
## Standards
- **TypeScript strict mode** is enabled in every package and app.
- **pnpm** is the only supported package manager — never run `npm install` or `yarn`.
- **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.
## 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.
When auth integration is needed, use placeholder TODO comments:
- `// @UseGuards(JwtAuthGuard) — TODO: integrate @edr/auth`
- `// 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.
## Port Assignments
- `edr-freight-api`: 3001
- `edr-freight-web/portal`: 5173
- `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
## Database Layout
- `postgres-freight` (port 5433): database `edr_freight` — freight 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.
## 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.
2. The entity extends `BaseEntity` 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`.
## Adding a new shared component to `@edr/ui-common`
1. Create `src/components/<Name>/<Name>.tsx` and `src/components/<Name>/index.ts`.
2. Export from `src/index.ts`.
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"
},
"jest": {
"moduleFileExtensions": ["js", "json", "ts"],
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": { "^.+\\.(t|j)s$": "ts-jest" },
"collectCoverageFrom": ["**/*.(t|j)s"],
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}

View File

@@ -1,17 +1,17 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
import appConfig from './config/app.config';
import databaseConfig from './config/database.config';
import appConfig from "./config/app.config";
import databaseConfig from "./config/database.config";
import { BookingsModule } from './modules/bookings/bookings.module';
import { ConsignmentsModule } from './modules/consignments/consignments.module';
import { TrainsModule } from './modules/trains/trains.module';
import { CustomersModule } from './modules/customers/customers.module';
import { TrackingModule } from './modules/tracking/tracking.module';
import { BillingModule } from './modules/billing/billing.module';
import { NotificationsModule } from './modules/notifications/notifications.module';
import { BookingsModule } from "./modules/bookings/bookings.module";
import { ConsignmentsModule } from "./modules/consignments/consignments.module";
import { TrainsModule } from "./modules/trains/trains.module";
import { CustomersModule } from "./modules/customers/customers.module";
import { TrackingModule } from "./modules/tracking/tracking.module";
import { BillingModule } from "./modules/billing/billing.module";
import { NotificationsModule } from "./modules/notifications/notifications.module";
@Module({
imports: [
@@ -22,7 +22,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService): TypeOrmModuleOptions =>
config.get<TypeOrmModuleOptions>('database')!,
config.get<TypeOrmModuleOptions>("database")!,
}),
BookingsModule,
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', () => ({
env: process.env.NODE_ENV ?? 'development',
port: parseInt(process.env.PORT ?? '3001', 10),
apiPrefix: 'api',
export default registerAs("app", () => ({
env: process.env.NODE_ENV ?? "development",
port: parseInt(process.env.PORT ?? "3001", 10),
apiPrefix: "api",
}));

View File

@@ -1,16 +1,19 @@
import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { registerAs } from "@nestjs/config";
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
export default registerAs('database', (): TypeOrmModuleOptions => ({
type: 'postgres',
host: process.env.DB_HOST ?? 'localhost',
port: parseInt(process.env.DB_PORT ?? '5433', 10),
username: process.env.DB_USER ?? 'postgres',
password: process.env.DB_PASSWORD ?? '',
database: process.env.DB_NAME ?? 'edr_freight',
entities: [__dirname + '/../**/*.entity.{ts,js}'],
migrations: [__dirname + '/../../migrations/*.{ts,js}'],
// Never enable synchronize in production. Use migrations.
synchronize: process.env.NODE_ENV === 'development',
logging: process.env.NODE_ENV === 'development',
}));
export default registerAs(
"database",
(): TypeOrmModuleOptions => ({
type: "postgres",
host: process.env.DB_HOST ?? "localhost",
port: parseInt(process.env.DB_PORT ?? "5433", 10),
username: process.env.DB_USER ?? "postgres",
password: process.env.DB_PASSWORD ?? "",
database: process.env.DB_NAME ?? "edr_freight",
entities: [__dirname + "/../**/*.entity.{ts,js}"],
migrations: [__dirname + "/../../migrations/*.{ts,js}"],
// 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 { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { HttpExceptionFilter, ResponseTransformInterceptor, createValidationPipe } from '@edr/api-common';
import "reflect-metadata";
import { NestFactory } from "@nestjs/core";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import {
HttpExceptionFilter,
ResponseTransformInterceptor,
createValidationPipe,
} from "@edr/api-common";
import { AppModule } from './app.module';
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
app.setGlobalPrefix('api');
app.setGlobalPrefix("api");
app.useGlobalPipes(createValidationPipe());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResponseTransformInterceptor());
const config = new DocumentBuilder()
.setTitle('EDR Freight API')
.setDescription('API for the EDR Freight Management application')
.setVersion('0.1.0')
.setTitle("EDR Freight API")
.setDescription("API for the EDR Freight Management application")
.setVersion("0.1.0")
.addBearerAuth()
.build();
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);
// eslint-disable-next-line no-console
console.log(`[freight-api] listening on http://localhost:${port}`);

View File

@@ -1,23 +1,23 @@
import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Controller, Get, Param, ParseUUIDPipe } from "@nestjs/common";
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
@Controller('billing')
@Controller("billing")
export class BillingController {
constructor(private readonly billingService: BillingService) {}
@Get('invoices')
@ApiOperation({ summary: 'List all invoices' })
@Get("invoices")
@ApiOperation({ summary: "List all invoices" })
findAll() {
return this.billingService.findAll();
}
@Get('invoices/booking/:bookingId')
@ApiOperation({ summary: 'List invoices for a booking' })
findByBooking(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
@Get("invoices/booking/:bookingId")
@ApiOperation({ summary: "List invoices for a booking" })
findByBooking(@Param("bookingId", ParseUUIDPipe) bookingId: string) {
return this.billingService.findByBooking(bookingId);
}
}

View File

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

View File

@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Invoice } from './entities/invoice.entity';
import { Invoice } from "./entities/invoice.entity";
@Injectable()
export class BillingService {
@@ -13,11 +13,14 @@ export class BillingService {
/** List every invoice (most recent first). */
findAll(): Promise<Invoice[]> {
return this.invoicesRepository.find({ order: { issuedAt: 'DESC' } });
return this.invoicesRepository.find({ order: { issuedAt: "DESC" } });
}
/** List invoices for a given booking. */
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 { Freight } from '@edr/types';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from "@edr/api-common";
import { Freight } from "@edr/types";
import { Column, Entity } from "typeorm";
@Entity({ name: 'invoices' })
@Entity({ name: "invoices" })
export class Invoice extends BaseEntity {
@Column({ name: 'booking_id', type: 'uuid' })
@Column({ name: "booking_id", type: "uuid" })
bookingId!: string;
@Column({ name: 'invoice_number', type: 'varchar', length: 64, unique: true })
@Column({ name: "invoice_number", type: "varchar", length: 64, unique: true })
invoiceNumber!: string;
@Column({ name: 'amount', type: 'numeric', precision: 14, scale: 2 })
@Column({ name: "amount", type: "numeric", precision: 14, scale: 2 })
amount!: number;
@Column({ name: 'currency', type: 'varchar', length: 8, default: 'ETB' })
@Column({ name: "currency", type: "varchar", length: 8, default: "ETB" })
currency!: string;
@Column({
name: 'status',
type: 'enum',
name: "status",
type: "enum",
enum: Freight.PaymentStatus,
default: Freight.PaymentStatus.Pending,
})
status!: Freight.PaymentStatus;
@Column({ name: 'issued_at', type: 'timestamptz' })
@Column({ name: "issued_at", type: "timestamptz" })
issuedAt!: Date;
@Column({ name: 'due_at', type: 'timestamptz' })
@Column({ name: "due_at", type: "timestamptz" })
dueAt!: Date;
}

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { BaseRepository } from '@edr/api-common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BaseRepository } from "@edr/api-common";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Booking } from './entities/booking.entity';
import { Booking } from "./entities/booking.entity";
@Injectable()
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 { CreateBookingDto } from './dto/create-booking.dto';
import { FilterBookingDto } from './dto/filter-booking.dto';
import { Booking } from './entities/booking.entity';
import { BookingsRepository } from "./bookings.repository";
import { CreateBookingDto } from "./dto/create-booking.dto";
import { FilterBookingDto } from "./dto/filter-booking.dto";
import { Booking } from "./entities/booking.entity";
@Injectable()
export class BookingsService {
@@ -18,7 +18,9 @@ export class BookingsService {
}
/** 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 pageSize = filter.pageSize ?? 20;
const [items, total] = await this.bookingsRepository.findAndCount({
@@ -28,7 +30,7 @@ export class BookingsService {
},
skip: (page - 1) * pageSize,
take: pageSize,
order: { createdAt: 'DESC' },
order: { createdAt: "DESC" },
});
return { items, total };
}

View File

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

View File

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

View File

@@ -1,35 +1,41 @@
import { BaseEntity } from '@edr/api-common';
import { Freight } from '@edr/types';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from "@edr/api-common";
import { Freight } from "@edr/types";
import { Column, Entity } from "typeorm";
@Entity({ name: 'bookings' })
@Entity({ name: "bookings" })
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;
@Column({ name: 'customer_id', type: 'uuid' })
@Column({ name: "customer_id", type: "uuid" })
customerId!: string;
@Column({ name: 'train_id', type: 'uuid', nullable: true })
@Column({ name: "train_id", type: "uuid", nullable: true })
trainId?: string | null;
@Column({
name: 'status',
type: 'enum',
name: "status",
type: "enum",
enum: Freight.BookingStatus,
default: Freight.BookingStatus.Draft,
})
status!: Freight.BookingStatus;
@Column({ name: 'scheduled_date', type: 'timestamptz' })
@Column({ name: "scheduled_date", type: "timestamptz" })
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;
@Column({
name: 'payment_status',
type: 'enum',
name: "payment_status",
type: "enum",
enum: Freight.PaymentStatus,
default: Freight.PaymentStatus.Pending,
})

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { BaseRepository } from '@edr/api-common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BaseRepository } from "@edr/api-common";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Consignment } from './entities/consignment.entity';
import { Consignment } from "./entities/consignment.entity";
@Injectable()
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 { CreateConsignmentDto } from './dto/create-consignment.dto';
import { FilterConsignmentDto } from './dto/filter-consignment.dto';
import { Consignment } from './entities/consignment.entity';
import { ConsignmentsRepository } from "./consignments.repository";
import { CreateConsignmentDto } from "./dto/create-consignment.dto";
import { FilterConsignmentDto } from "./dto/filter-consignment.dto";
import { Consignment } from "./entities/consignment.entity";
@Injectable()
export class ConsignmentsService {
constructor(private readonly consignmentsRepository: ConsignmentsRepository) {}
constructor(
private readonly consignmentsRepository: ConsignmentsRepository,
) {}
/** Create a new consignment for a freight booking. */
create(dto: CreateConsignmentDto): Promise<Consignment> {
@@ -15,7 +17,9 @@ export class ConsignmentsService {
}
/** 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 pageSize = filter.pageSize ?? 20;
const [items, total] = await this.consignmentsRepository.findAndCount({
@@ -25,7 +29,7 @@ export class ConsignmentsService {
},
skip: (page - 1) * pageSize,
take: pageSize,
order: { createdAt: 'DESC' },
order: { createdAt: "DESC" },
});
return { items, total };
}

View File

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

View File

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

View File

@@ -1,32 +1,37 @@
import { BaseEntity } from '@edr/api-common';
import { Freight } from '@edr/types';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from "@edr/api-common";
import { Freight } from "@edr/types";
import { Column, Entity } from "typeorm";
@Entity({ name: 'consignments' })
@Entity({ name: "consignments" })
export class Consignment extends BaseEntity {
@Column({ name: 'booking_id', type: 'uuid' })
@Column({ name: "booking_id", type: "uuid" })
bookingId!: string;
@Column({ name: 'tracking_number', type: 'varchar', length: 64, unique: true })
@Column({
name: "tracking_number",
type: "varchar",
length: 64,
unique: true,
})
trackingNumber!: string;
@Column({ name: 'cargo_type', type: 'enum', enum: Freight.CargoType })
@Column({ name: "cargo_type", type: "enum", enum: 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;
@Column({
name: 'status',
type: 'enum',
name: "status",
type: "enum",
enum: Freight.ConsignmentStatus,
default: Freight.ConsignmentStatus.Pending,
})
status!: Freight.ConsignmentStatus;
@Column({ name: 'origin_station', type: 'varchar', length: 128 })
@Column({ name: "origin_station", type: "varchar", length: 128 })
originStation!: string;
@Column({ name: 'destination_station', type: 'varchar', length: 128 })
@Column({ name: "destination_station", type: "varchar", length: 128 })
destinationStation!: string;
}

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { BaseRepository } from '@edr/api-common';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { BaseRepository } from "@edr/api-common";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Customer } from './entities/customer.entity';
import { Customer } from "./entities/customer.entity";
@Injectable()
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 { CreateCustomerDto } from './dto/create-customer.dto';
import { Customer } from './entities/customer.entity';
import { CustomersRepository } from "./customers.repository";
import { CreateCustomerDto } from "./dto/create-customer.dto";
import { Customer } from "./entities/customer.entity";
@Injectable()
export class CustomersService {
@@ -15,7 +15,7 @@ export class CustomersService {
/** List every customer (alphabetical). */
findAll(): Promise<Customer[]> {
return this.customersRepository.findAll({ order: { name: 'ASC' } });
return this.customersRepository.findAll({ order: { name: "ASC" } });
}
/** 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 {
@IsString()

View File

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

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({
providers: [NotificationsService],

View File

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

View File

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

View File

@@ -1,17 +1,19 @@
import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Controller, Get, Param, ParseUUIDPipe } from "@nestjs/common";
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
@Controller('tracking')
@Controller("tracking")
export class TrackingController {
constructor(private readonly trackingService: TrackingService) {}
@Get(':consignmentId')
@ApiOperation({ summary: 'Get the tracking timeline for a consignment' })
findByConsignment(@Param('consignmentId', ParseUUIDPipe) consignmentId: string) {
@Get(":consignmentId")
@ApiOperation({ summary: "Get the tracking timeline for a consignment" })
findByConsignment(
@Param("consignmentId", ParseUUIDPipe) consignmentId: string,
) {
return this.trackingService.findByConsignment(consignmentId);
}
}

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,23 @@
import { BaseEntity } from '@edr/api-common';
import { Freight } from '@edr/types';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from "@edr/api-common";
import { Freight } from "@edr/types";
import { Column, Entity } from "typeorm";
@Entity({ name: 'trains' })
@Entity({ name: "trains" })
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;
@Column({ name: 'capacity_tons', type: 'numeric', precision: 10, scale: 2 })
@Column({ name: "capacity_tons", type: "numeric", precision: 10, scale: 2 })
capacityTons!: number;
@Column({
name: 'status',
type: 'enum',
name: "status",
type: "enum",
enum: Freight.TrainStatus,
default: Freight.TrainStatus.Available,
})
status!: Freight.TrainStatus;
@Column({ name: 'notes', type: 'text', nullable: true })
@Column({ name: "notes", type: "text", nullable: true })
notes?: string | null;
}

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest';
import { INestApplication } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
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;
beforeAll(async () => {
@@ -20,7 +20,7 @@ describe('Freight API (e2e)', () => {
await app.close();
});
it('GET /api/bookings returns a 200 with a list', () => {
return request(app.getHttpServer()).get('/api/bookings').expect(200);
it("GET /api/bookings returns a 200 with a list", () => {
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 { DashboardLayout, type SidebarItem } from '@edr/ui-common';
import {
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[] = [
{ label: 'Dashboard', href: '/' },
];
const sidebarItems: SidebarItem[] = [{ label: "Dashboard", href: "/" }];
const App = () => {
const navigate = useNavigate();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import type { Freight } from '@edr/types';
import { Badge } from '@edr/ui-common';
import type { Freight } from "@edr/types";
import { Badge } from "@edr/ui-common";
export interface TrackingTimelineProps {
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 = () =>
useQuery({
queryKey: ['bookings'],
queryKey: ["bookings"],
queryFn: bookingsService.list,
});
export const useBooking = (id: string) =>
useQuery({
queryKey: ['bookings', id],
queryKey: ["bookings", id],
queryFn: () => bookingsService.get(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 = () =>
useQuery({
queryKey: ['consignments'],
queryKey: ["consignments"],
queryFn: consignmentsService.list,
});
export const useConsignment = (id: string) =>
useQuery({
queryKey: ['consignments', id],
queryKey: ["consignments", id],
queryFn: () => consignmentsService.get(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) =>
useQuery({
queryKey: ['tracking', consignmentId],
queryKey: ["tracking", consignmentId],
queryFn: () => trackingService.forConsignment(consignmentId),
enabled: Boolean(consignmentId),
});

View File

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

View File

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

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 { 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 (!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 (
<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">
<dt className="text-gray-500">Status</dt>
<dd className="text-gray-900">{booking.status}</dd>
<dt className="text-gray-500">Customer ID</dt>
<dd className="text-gray-900">{booking.customerId}</dd>
<dt className="text-gray-500">Scheduled</dt>
<dd className="text-gray-900">{new Date(booking.scheduledDate).toLocaleString()}</dd>
<dd className="text-gray-900">
{new Date(booking.scheduledDate).toLocaleString()}
</dd>
<dt className="text-gray-500">Total amount</dt>
<dd className="text-gray-900">{booking.totalAmount.toFixed(2)}</dd>
</dl>

View File

@@ -1,8 +1,8 @@
import { Link } from 'react-router-dom';
import { Button } from '@edr/ui-common';
import { Link } from "react-router-dom";
import { Button } from "@edr/ui-common";
import BookingTable from '../../components/bookings/BookingTable';
import { useBookings } from '../../hooks/useBookings';
import BookingTable from "../../components/bookings/BookingTable";
import { useBookings } from "../../hooks/useBookings";
const BookingsPage = () => {
const { data, isLoading } = useBookings();
@@ -16,7 +16,11 @@ const BookingsPage = () => {
<Button>New booking</Button>
</Link>
</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>
);
};

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,8 @@ const TrainsPage = () => (
<div className="flex flex-col gap-4">
<h1 className="text-2xl font-semibold text-gray-900">Trains</h1>
<p className="text-sm text-gray-600">
Fleet roster, capacity, and maintenance status. Connect to <code>/trains</code> when ready.
Fleet roster, capacity, and maintenance status. Connect to{" "}
<code>/trains</code> when ready.
</p>
</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 {
reference: string;
@@ -12,7 +12,7 @@ export interface CreateBookingPayload {
export const bookingsService = {
list: async (): Promise<PaginatedResponse<Freight.IBooking>> => {
const { data } = await api.get('/bookings');
const { data } = await api.get("/bookings");
return data.data;
},
get: async (id: string): Promise<Freight.IBooking> => {
@@ -20,7 +20,7 @@ export const bookingsService = {
return data.data;
},
create: async (payload: CreateBookingPayload): Promise<Freight.IBooking> => {
const { data } = await api.post('/bookings', payload);
const { data } = await api.post("/bookings", payload);
return data.data;
},
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 = {
list: async (): Promise<PaginatedResponse<Freight.IConsignment>> => {
const { data } = await api.get('/consignments');
const { data } = await api.get("/consignments");
return data.data;
},
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 = {
forConsignment: async (consignmentId: string): Promise<Freight.ITrackingEvent[]> => {
forConsignment: async (
consignmentId: string,
): Promise<Freight.ITrackingEvent[]> => {
const { data } = await api.get(`/tracking/${consignmentId}`);
return data.data;
},

View File

@@ -1,4 +1,4 @@
import { create } from 'zustand';
import { create } from "zustand";
interface AppState {
// TODO: integrate @edr/auth — currentUser will be sourced from the auth package
@@ -8,5 +8,6 @@ interface AppState {
export const useAppStore = create<AppState>((set) => ({
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 {
href: string;

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule, TypeOrmModuleOptions } from "@nestjs/typeorm";
import appConfig from './config/app.config';
import databaseConfig from './config/database.config';
import appConfig from "./config/app.config";
import databaseConfig from "./config/database.config";
import { TicketsModule } from './modules/tickets/tickets.module';
import { SchedulesModule } from './modules/schedules/schedules.module';
import { PassengersModule } from './modules/passengers/passengers.module';
import { SeatsModule } from './modules/seats/seats.module';
import { StationsModule } from './modules/stations/stations.module';
import { PaymentsModule } from './modules/payments/payments.module';
import { NotificationsModule } from './modules/notifications/notifications.module';
import { TicketsModule } from "./modules/tickets/tickets.module";
import { SchedulesModule } from "./modules/schedules/schedules.module";
import { PassengersModule } from "./modules/passengers/passengers.module";
import { SeatsModule } from "./modules/seats/seats.module";
import { StationsModule } from "./modules/stations/stations.module";
import { PaymentsModule } from "./modules/payments/payments.module";
import { NotificationsModule } from "./modules/notifications/notifications.module";
@Module({
imports: [
@@ -22,7 +22,7 @@ import { NotificationsModule } from './modules/notifications/notifications.modul
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService): TypeOrmModuleOptions =>
config.get<TypeOrmModuleOptions>('database')!,
config.get<TypeOrmModuleOptions>("database")!,
}),
TicketsModule,
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', () => ({
env: process.env.NODE_ENV ?? 'development',
port: parseInt(process.env.PORT ?? '3002', 10),
apiPrefix: 'api',
export default registerAs("app", () => ({
env: process.env.NODE_ENV ?? "development",
port: parseInt(process.env.PORT ?? "3002", 10),
apiPrefix: "api",
}));

View File

@@ -1,15 +1,18 @@
import { registerAs } from '@nestjs/config';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { registerAs } from "@nestjs/config";
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
export default registerAs('database', (): TypeOrmModuleOptions => ({
type: 'postgres',
host: process.env.DB_HOST ?? 'localhost',
port: parseInt(process.env.DB_PORT ?? '5434', 10),
username: process.env.DB_USER ?? 'postgres',
password: process.env.DB_PASSWORD ?? '',
database: process.env.DB_NAME ?? 'edr_passenger',
entities: [__dirname + '/../**/*.entity.{ts,js}'],
migrations: [__dirname + '/../../migrations/*.{ts,js}'],
synchronize: process.env.NODE_ENV === 'development',
logging: process.env.NODE_ENV === 'development',
}));
export default registerAs(
"database",
(): TypeOrmModuleOptions => ({
type: "postgres",
host: process.env.DB_HOST ?? "localhost",
port: parseInt(process.env.DB_PORT ?? "5434", 10),
username: process.env.DB_USER ?? "postgres",
password: process.env.DB_PASSWORD ?? "",
database: process.env.DB_NAME ?? "edr_passenger",
entities: [__dirname + "/../**/*.entity.{ts,js}"],
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 { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { HttpExceptionFilter, ResponseTransformInterceptor, createValidationPipe } from '@edr/api-common';
import "reflect-metadata";
import { NestFactory } from "@nestjs/core";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import {
HttpExceptionFilter,
ResponseTransformInterceptor,
createValidationPipe,
} from "@edr/api-common";
import { AppModule } from './app.module';
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
app.setGlobalPrefix('api');
app.setGlobalPrefix("api");
app.useGlobalPipes(createValidationPipe());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new ResponseTransformInterceptor());
const config = new DocumentBuilder()
.setTitle('EDR Passenger API')
.setDescription('API for the EDR Passenger Management application')
.setVersion('0.1.0')
.setTitle("EDR Passenger API")
.setDescription("API for the EDR Passenger Management application")
.setVersion("0.1.0")
.addBearerAuth()
.build();
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);
// eslint-disable-next-line no-console
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({
providers: [NotificationsService],

View File

@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger } from "@nestjs/common";
@Injectable()
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 {
@IsString()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
import { Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Controller, Get, Param, ParseUUIDPipe } from "@nestjs/common";
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
@Controller('payments')
@Controller("payments")
export class PaymentsController {
constructor(private readonly paymentsService: PaymentsService) {}
@Get('ticket/:ticketId')
@ApiOperation({ summary: 'List payments for a ticket' })
findByTicket(@Param('ticketId', ParseUUIDPipe) ticketId: string) {
@Get("ticket/:ticketId")
@ApiOperation({ summary: "List payments for a ticket" })
findByTicket(@Param("ticketId", ParseUUIDPipe) ticketId: string) {
return this.paymentsService.findByTicket(ticketId);
}
}

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,32 @@
import { BaseEntity } from '@edr/api-common';
import { Passenger } from '@edr/types';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from "@edr/api-common";
import { Passenger } from "@edr/types";
import { Column, Entity } from "typeorm";
@Entity({ name: 'schedules' })
@Entity({ name: "schedules" })
export class Schedule extends BaseEntity {
@Column({ name: 'train_code', type: 'varchar', length: 32 })
@Column({ name: "train_code", type: "varchar", length: 32 })
trainCode!: string;
@Column({ name: 'origin_station_id', type: 'uuid' })
@Column({ name: "origin_station_id", type: "uuid" })
originStationId!: string;
@Column({ name: 'destination_station_id', type: 'uuid' })
@Column({ name: "destination_station_id", type: "uuid" })
destinationStationId!: string;
@Column({ name: 'departure_time', type: 'timestamptz' })
@Column({ name: "departure_time", type: "timestamptz" })
departureTime!: Date;
@Column({ name: 'arrival_time', type: 'timestamptz' })
@Column({ name: "arrival_time", type: "timestamptz" })
arrivalTime!: Date;
@Column({
name: 'status',
type: 'enum',
name: "status",
type: "enum",
enum: Passenger.ScheduleStatus,
default: Passenger.ScheduleStatus.Scheduled,
})
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;
}

View File

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

View File

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

View File

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

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