feat: add transit microservice and supervisor dashboard

This commit is contained in:
Hamza-Ayed
2026-07-18 18:18:40 +03:00
parent 4aeab5275a
commit f30209b1d7
43 changed files with 3642 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TransitModule } from './transit/transit.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (cfg: ConfigService) => ({
type: 'postgres',
host: cfg.get<string>('DB_HOST'),
port: cfg.get<number>('DB_PORT'),
database: cfg.get<string>('DB_NAME'),
username: cfg.get<string>('DB_USER'),
password: cfg.get<string>('DB_PASSWORD'),
synchronize: true, // Should be false in production
autoLoadEntities: true,
}),
}),
TransitModule,
],
})
export class AppModule {}
+18
View File
@@ -0,0 +1,18 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const cfg = app.get(ConfigService);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.enableCors();
// Start the standalone transit API
const port = cfg.get<number>('PORT') || 4020;
await app.listen(port);
console.log(`Transit Microservice running on port ${port}`);
}
bootstrap();
@@ -0,0 +1,32 @@
export class CreateRouteDto {
name: string;
tenant_id: string;
polyline?: Record<string, any>;
is_active?: boolean;
}
export class CreateStationDto {
route_id: string;
name: string;
latitude: number;
longitude: number;
order_index: number;
}
export class CreateSupervisorDto {
tenant_id: string;
user_id: string;
route_id?: string;
institution_name?: string;
}
export class StartTripDto {
route_id: string;
driver_id: string;
}
export class BuyTicketDto {
passenger_id: string;
bus_trip_id: string;
price: number;
}
@@ -0,0 +1,33 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { TransitRoute } from './transit-route.entity';
export type BusTripStatus = 'active' | 'completed' | 'cancelled';
@Entity('transit_bus_trips')
export class TransitBusTrip {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
route_id: string;
// External reference to the main DB user
@Column()
driver_id: string;
@Column({ type: 'varchar', length: 50, default: 'active' })
status: BusTripStatus;
@Column({ nullable: true })
current_station_id: string;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
@ManyToOne(() => TransitRoute)
@JoinColumn({ name: 'route_id' })
route: TransitRoute;
}
@@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('transit_routes')
export class TransitRoute {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
tenant_id: string;
@Column({ length: 150 })
name: string;
@Column({ type: 'jsonb', nullable: true })
polyline: Record<string, any>;
@Column({ default: true })
is_active: boolean;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
}
@@ -0,0 +1,30 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { TransitRoute } from './transit-route.entity';
@Entity('transit_stations')
export class TransitStation {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
route_id: string;
@Column({ length: 150 })
name: string;
@Column('decimal', { precision: 10, scale: 7 })
latitude: number;
@Column('decimal', { precision: 10, scale: 7 })
longitude: number;
@Column('int')
order_index: number;
@CreateDateColumn()
created_at: Date;
@ManyToOne(() => TransitRoute)
@JoinColumn({ name: 'route_id' })
route: TransitRoute;
}
@@ -0,0 +1,28 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { TransitRoute } from './transit-route.entity';
@Entity('transit_supervisors')
export class TransitSupervisor {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
tenant_id: string;
// External reference to the main DB user
@Column()
user_id: string;
@Column({ nullable: true })
route_id: string;
@Column({ length: 150, nullable: true })
institution_name: string;
@CreateDateColumn()
created_at: Date;
@ManyToOne(() => TransitRoute)
@JoinColumn({ name: 'route_id' })
route: TransitRoute;
}
@@ -0,0 +1,33 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { TransitBusTrip } from './transit-bus-trip.entity';
export type TicketStatus = 'active' | 'used' | 'refunded';
@Entity('transit_tickets')
export class TransitTicket {
@PrimaryGeneratedColumn('uuid')
id: string;
// External reference to the main DB user
@Column()
passenger_id: string;
@Column()
bus_trip_id: string;
@Column('decimal', { precision: 10, scale: 3 })
price: number;
@Column({ type: 'varchar', length: 50, default: 'active' })
status: TicketStatus;
@Column({ nullable: true })
qr_code: string;
@CreateDateColumn()
created_at: Date;
@ManyToOne(() => TransitBusTrip)
@JoinColumn({ name: 'bus_trip_id' })
bus_trip: TransitBusTrip;
}
@@ -0,0 +1,53 @@
import { Controller, Post, Get, Body, Param, Query } from '@nestjs/common';
import { TransitService } from './transit.service';
import { CreateRouteDto, CreateStationDto, CreateSupervisorDto, StartTripDto, BuyTicketDto } from './dto/transit.dto';
@Controller('transit')
export class TransitController {
constructor(private readonly transitService: TransitService) {}
@Post('routes')
createRoute(@Body() dto: CreateRouteDto) {
return this.transitService.createRoute(dto);
}
@Get('routes')
getRoutes(@Query('tenant_id') tenant_id: string) {
return this.transitService.getRoutes(tenant_id);
}
@Post('stations')
createStation(@Body() dto: CreateStationDto) {
return this.transitService.createStation(dto);
}
@Get('routes/:routeId/stations')
getStations(@Param('routeId') routeId: string) {
return this.transitService.getStations(routeId);
}
@Post('supervisors')
assignSupervisor(@Body() dto: CreateSupervisorDto) {
return this.transitService.assignSupervisor(dto);
}
@Get('reports/supervisor/:userId')
getSupervisorReports(@Param('userId') userId: string) {
return this.transitService.getSupervisorReports(userId);
}
@Post('trips/start')
startTrip(@Body() dto: StartTripDto) {
return this.transitService.startTrip(dto);
}
@Post('trips/:id/end')
endTrip(@Param('id') id: string) {
return this.transitService.endTrip(id);
}
@Post('tickets/buy')
buyTicket(@Body() dto: BuyTicketDto) {
return this.transitService.buyTicket(dto);
}
}
@@ -0,0 +1,25 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TransitRoute } from './entities/transit-route.entity';
import { TransitStation } from './entities/transit-station.entity';
import { TransitSupervisor } from './entities/transit-supervisor.entity';
import { TransitBusTrip } from './entities/transit-bus-trip.entity';
import { TransitTicket } from './entities/transit-ticket.entity';
import { TransitController } from './transit.controller';
import { TransitService } from './transit.service';
@Module({
imports: [
TypeOrmModule.forFeature([
TransitRoute,
TransitStation,
TransitSupervisor,
TransitBusTrip,
TransitTicket,
]),
],
controllers: [TransitController],
providers: [TransitService],
exports: [TypeOrmModule, TransitService],
})
export class TransitModule {}
@@ -0,0 +1,74 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TransitRoute } from './entities/transit-route.entity';
import { TransitStation } from './entities/transit-station.entity';
import { TransitSupervisor } from './entities/transit-supervisor.entity';
import { TransitBusTrip } from './entities/transit-bus-trip.entity';
import { TransitTicket } from './entities/transit-ticket.entity';
import { CreateRouteDto, CreateStationDto, CreateSupervisorDto, StartTripDto, BuyTicketDto } from './dto/transit.dto';
@Injectable()
export class TransitService {
constructor(
@InjectRepository(TransitRoute) private routesRepo: Repository<TransitRoute>,
@InjectRepository(TransitStation) private stationsRepo: Repository<TransitStation>,
@InjectRepository(TransitSupervisor) private supervisorsRepo: Repository<TransitSupervisor>,
@InjectRepository(TransitBusTrip) private tripsRepo: Repository<TransitBusTrip>,
@InjectRepository(TransitTicket) private ticketsRepo: Repository<TransitTicket>,
) {}
// Routes
async createRoute(dto: CreateRouteDto) {
const route = this.routesRepo.create(dto);
return this.routesRepo.save(route);
}
async getRoutes(tenant_id: string) {
return this.routesRepo.find({ where: { tenant_id, is_active: true } });
}
// Stations
async createStation(dto: CreateStationDto) {
const station = this.stationsRepo.create(dto);
return this.stationsRepo.save(station);
}
async getStations(route_id: string) {
return this.stationsRepo.find({ where: { route_id }, order: { order_index: 'ASC' } });
}
// Supervisors
async assignSupervisor(dto: CreateSupervisorDto) {
const supervisor = this.supervisorsRepo.create(dto);
return this.supervisorsRepo.save(supervisor);
}
async getSupervisorReports(user_id: string) {
// Basic implementation: find trips and tickets for the supervisor's route
const supervisor = await this.supervisorsRepo.findOne({ where: { user_id } });
if (!supervisor || !supervisor.route_id) throw new NotFoundException('Supervisor or Route not found');
const trips = await this.tripsRepo.count({ where: { route_id: supervisor.route_id } });
// This requires a more complex query in production, but simplified for scaffolding
return { supervisor_id: supervisor.id, route_id: supervisor.route_id, total_trips: trips };
}
// Trips
async startTrip(dto: StartTripDto) {
const trip = this.tripsRepo.create({ ...dto, status: 'active' });
return this.tripsRepo.save(trip);
}
async endTrip(id: string) {
await this.tripsRepo.update(id, { status: 'completed' });
return this.tripsRepo.findOne({ where: { id } });
}
// Tickets
async buyTicket(dto: BuyTicketDto) {
// Future: Call main API to deduct from wallet
const ticket = this.ticketsRepo.create({ ...dto, status: 'active' });
return this.ticketsRepo.save(ticket);
}
}