feat: add transit microservice and supervisor dashboard
This commit is contained in:
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user