diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index ec1664f15..dceb23f2f 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -60,6 +60,8 @@ import { RoutesModule } from './modules/routes/routes.module'; import { WarehousesModule } from './modules/warehouses/warehouses.module'; import { FacilitiesModule } from './modules/facilities/facilities.module'; import { OverviewModule } from './modules/overview/overview.module'; +import { VehiclesModule } from './modules/vehicles/vehicles.module'; +import { DriversModule } from './modules/drivers/drivers.module'; @Module({ imports: [ @@ -119,6 +121,8 @@ import { OverviewModule } from './modules/overview/overview.module'; FacilitiesModule, WarehousesModule, OverviewModule, + VehiclesModule, + DriversModule, ], providers: [ EdrOrgSeeder, diff --git a/apps/edr-freight-api/src/migrations/1770000000000-CreateVehiclesTable.ts b/apps/edr-freight-api/src/migrations/1770000000000-CreateVehiclesTable.ts new file mode 100644 index 000000000..2cf09c9f5 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1770000000000-CreateVehiclesTable.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateVehiclesTable1770000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'vehicles' AND table_schema = 'freight') THEN + CREATE TABLE freight.vehicles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + plate_number VARCHAR NOT NULL UNIQUE, + registration_number VARCHAR NOT NULL UNIQUE, + vehicle_type VARCHAR NOT NULL, + manufacturer VARCHAR NOT NULL, + model VARCHAR NOT NULL, + year INTEGER NOT NULL, + fuel_type VARCHAR NOT NULL, + capacity NUMERIC NOT NULL, + status VARCHAR DEFAULT 'ACTIVE' NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted_at TIMESTAMP NULL + ); + + CREATE INDEX idx_vehicles_plate_number ON freight.vehicles(plate_number); + CREATE INDEX idx_vehicles_registration_number ON freight.vehicles(registration_number); + CREATE INDEX idx_vehicles_status ON freight.vehicles(status); + CREATE INDEX idx_vehicles_vehicle_type ON freight.vehicles(vehicle_type); + CREATE INDEX idx_vehicles_manufacturer ON freight.vehicles(manufacturer); + END IF; + END $$; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS freight.vehicles CASCADE;`); + } +} diff --git a/apps/edr-freight-api/src/migrations/1775000000000-CreateDriversTable.ts b/apps/edr-freight-api/src/migrations/1775000000000-CreateDriversTable.ts new file mode 100644 index 000000000..8f41587cf --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1775000000000-CreateDriversTable.ts @@ -0,0 +1,42 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateDriversTable1775000000000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'drivers' AND table_schema = 'freight') THEN + CREATE TABLE freight.drivers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + license_number VARCHAR NOT NULL UNIQUE, + first_name VARCHAR NOT NULL, + last_name VARCHAR NOT NULL, + email VARCHAR NOT NULL UNIQUE, + phone_number VARCHAR NOT NULL UNIQUE, + date_of_birth DATE NOT NULL, + license_expiry_date DATE NOT NULL, + status VARCHAR DEFAULT 'ACTIVE' NOT NULL, + vehicle_types_authorized VARCHAR[], + address TEXT, + emergency_contact VARCHAR, + notes TEXT, + total_trips INTEGER DEFAULT 0, + rating NUMERIC(3, 2), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + deleted_at TIMESTAMP NULL + ); + + CREATE INDEX idx_drivers_license_number ON freight.drivers(license_number); + CREATE INDEX idx_drivers_email ON freight.drivers(email); + CREATE INDEX idx_drivers_phone_number ON freight.drivers(phone_number); + CREATE INDEX idx_drivers_status ON freight.drivers(status); + END IF; + END $$; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE IF EXISTS freight.drivers CASCADE;`); + } +} diff --git a/apps/edr-freight-api/src/modules/drivers/drivers.controller.ts b/apps/edr-freight-api/src/modules/drivers/drivers.controller.ts new file mode 100644 index 000000000..eb0628b96 --- /dev/null +++ b/apps/edr-freight-api/src/modules/drivers/drivers.controller.ts @@ -0,0 +1,74 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Param, + Body, + Query, + ParseUUIDPipe, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FleetManage, FleetView } from '../../common/booking-guards'; +import { DriversService } from './drivers.service'; +import { CreateDriverDto } from './dto/create-driver.dto'; +import { UpdateDriverDto } from './dto/update-driver.dto'; + +@ApiTags('drivers') +@ApiBearerAuth() +@Controller('drivers') +@FleetView() +export class DriversController { + constructor(private readonly driversService: DriversService) {} + + @Post() + @FleetManage() + @ApiOperation({ summary: 'Create a new driver' }) + create(@Body() createDriverDto: CreateDriverDto) { + return this.driversService.create(createDriverDto); + } + + @Get() + @ApiOperation({ summary: 'Get all drivers with filters' }) + findAll( + @Query('search') search?: string, + @Query('status') status?: string, + @Query('page') page?: string, + @Query('limit') limit?: string, + @Query('sortBy') sortBy?: string, + @Query('sortOrder') sortOrder?: 'ASC' | 'DESC', + ) { + return this.driversService.findAll({ + search, + status: status as any, + page: page ? parseInt(page) : undefined, + limit: limit ? parseInt(limit) : undefined, + sortBy, + sortOrder, + }); + } + + @Get(':id') + @ApiOperation({ summary: 'Get driver by id' }) + findOne(@Param('id', ParseUUIDPipe) id: string) { + return this.driversService.findById(id); + } + + @Patch(':id') + @FleetManage() + @ApiOperation({ summary: 'Update a driver' }) + update( + @Param('id', ParseUUIDPipe) id: string, + @Body() updateDriverDto: UpdateDriverDto, + ) { + return this.driversService.update(id, updateDriverDto); + } + + @Delete(':id') + @FleetManage() + @ApiOperation({ summary: 'Delete a driver' }) + remove(@Param('id', ParseUUIDPipe) id: string) { + return this.driversService.remove(id); + } +} diff --git a/apps/edr-freight-api/src/modules/drivers/drivers.module.ts b/apps/edr-freight-api/src/modules/drivers/drivers.module.ts new file mode 100644 index 000000000..9e685dcd6 --- /dev/null +++ b/apps/edr-freight-api/src/modules/drivers/drivers.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Driver } from './entities/driver.entity'; +import { DriversService } from './drivers.service'; +import { DriversController } from './drivers.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Driver])], + providers: [DriversService], + controllers: [DriversController], + exports: [DriversService], +}) +export class DriversModule {} diff --git a/apps/edr-freight-api/src/modules/drivers/drivers.service.ts b/apps/edr-freight-api/src/modules/drivers/drivers.service.ts new file mode 100644 index 000000000..5bc7dba69 --- /dev/null +++ b/apps/edr-freight-api/src/modules/drivers/drivers.service.ts @@ -0,0 +1,125 @@ +import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateDriverDto } from './dto/create-driver.dto'; +import { UpdateDriverDto } from './dto/update-driver.dto'; +import { Driver, DriverStatus } from './entities/driver.entity'; + +@Injectable() +export class DriversService { + constructor( + @InjectRepository(Driver) + private readonly driverRepo: Repository, + ) {} + + async create(dto: CreateDriverDto): Promise { + const existing = await this.driverRepo.findOne({ + where: [ + { licenseNumber: dto.licenseNumber }, + { email: dto.email }, + { phoneNumber: dto.phoneNumber }, + ], + }); + + if (existing) { + if (existing.licenseNumber === dto.licenseNumber) { + throw new ConflictException(`Driver with license number ${dto.licenseNumber} already exists`); + } + if (existing.email === dto.email) { + throw new ConflictException(`Driver with email ${dto.email} already exists`); + } + if (existing.phoneNumber === dto.phoneNumber) { + throw new ConflictException(`Driver with phone number ${dto.phoneNumber} already exists`); + } + } + + const driver = this.driverRepo.create(dto); + return this.driverRepo.save(driver); + } + + async findAll(query: { + search?: string; + status?: DriverStatus | string; + page?: number; + limit?: number; + sortBy?: string; + sortOrder?: 'ASC' | 'DESC'; + } = {}): Promise<{ data: Driver[]; total: number; page: number; limit: number }> { + const page = query.page || 1; + const limit = query.limit || 10; + const skip = (page - 1) * limit; + + let qb = this.driverRepo.createQueryBuilder('d'); + + if (query.search) { + qb = qb.where( + 'd.firstName ILIKE :search OR d.lastName ILIKE :search OR d.email ILIKE :search OR d.licenseNumber ILIKE :search OR d.phoneNumber ILIKE :search', + { search: `%${query.search}%` }, + ); + } + + if (query.status) { + qb = qb.andWhere('d.status = :status', { status: query.status }); + } + + const sortBy = ['firstName', 'lastName', 'status', 'createdAt'].includes(query.sortBy ?? '') + ? query.sortBy + : 'createdAt'; + const sortOrder = (query.sortOrder ?? 'DESC').toUpperCase(); + + const [data, total] = await qb + .orderBy(`d.${sortBy}`, sortOrder as 'ASC' | 'DESC') + .skip(skip) + .take(limit) + .getManyAndCount(); + + return { data, total, page, limit }; + } + + async findById(id: string): Promise { + const driver = await this.driverRepo.findOne({ where: { id } }); + if (!driver) { + throw new NotFoundException(`Driver ${id} not found`); + } + return driver; + } + + async update(id: string, dto: UpdateDriverDto): Promise { + const driver = await this.findById(id); + + if (dto.licenseNumber && dto.licenseNumber !== driver.licenseNumber) { + const existing = await this.driverRepo.findOne({ + where: { licenseNumber: dto.licenseNumber }, + }); + if (existing) { + throw new ConflictException(`Driver with license number ${dto.licenseNumber} already exists`); + } + } + + if (dto.email && dto.email !== driver.email) { + const existing = await this.driverRepo.findOne({ + where: { email: dto.email }, + }); + if (existing) { + throw new ConflictException(`Driver with email ${dto.email} already exists`); + } + } + + if (dto.phoneNumber && dto.phoneNumber !== driver.phoneNumber) { + const existing = await this.driverRepo.findOne({ + where: { phoneNumber: dto.phoneNumber }, + }); + if (existing) { + throw new ConflictException(`Driver with phone number ${dto.phoneNumber} already exists`); + } + } + + Object.assign(driver, dto); + return this.driverRepo.save(driver); + } + + async remove(id: string): Promise { + await this.findById(id); + await this.driverRepo.softDelete(id); + } +} diff --git a/apps/edr-freight-api/src/modules/drivers/dto/create-driver.dto.ts b/apps/edr-freight-api/src/modules/drivers/dto/create-driver.dto.ts new file mode 100644 index 000000000..d8e2aec4a --- /dev/null +++ b/apps/edr-freight-api/src/modules/drivers/dto/create-driver.dto.ts @@ -0,0 +1,45 @@ +import { IsString, IsEmail, IsDateString, IsEnum, IsOptional, IsArray } from 'class-validator'; +import { DriverStatus } from '../entities/driver.entity'; + +export class CreateDriverDto { + @IsString() + licenseNumber!: string; + + @IsString() + firstName!: string; + + @IsString() + lastName!: string; + + @IsEmail() + email!: string; + + @IsString() + phoneNumber!: string; + + @IsDateString() + dateOfBirth!: string; + + @IsDateString() + licenseExpiryDate!: string; + + @IsEnum(DriverStatus) + status!: DriverStatus; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + vehicleTypesAuthorized?: string[]; + + @IsOptional() + @IsString() + address?: string; + + @IsOptional() + @IsString() + emergencyContact?: string; + + @IsOptional() + @IsString() + notes?: string; +} diff --git a/apps/edr-freight-api/src/modules/drivers/dto/update-driver.dto.ts b/apps/edr-freight-api/src/modules/drivers/dto/update-driver.dto.ts new file mode 100644 index 000000000..f44e5410a --- /dev/null +++ b/apps/edr-freight-api/src/modules/drivers/dto/update-driver.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateDriverDto } from './create-driver.dto'; + +export class UpdateDriverDto extends PartialType(CreateDriverDto) {} diff --git a/apps/edr-freight-api/src/modules/drivers/entities/driver.entity.ts b/apps/edr-freight-api/src/modules/drivers/entities/driver.entity.ts new file mode 100644 index 000000000..7b1c40201 --- /dev/null +++ b/apps/edr-freight-api/src/modules/drivers/entities/driver.entity.ts @@ -0,0 +1,58 @@ +import { Entity, Column, Index } from 'typeorm'; +import { BaseEntity } from '@edr/api-common'; + +export enum DriverStatus { + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', + SUSPENDED = 'SUSPENDED', + ON_LEAVE = 'ON_LEAVE', +} + +@Entity({ name: 'drivers', schema: 'freight' }) +@Index(['licenseNumber']) +@Index(['email']) +@Index(['phoneNumber']) +@Index(['status']) +export class Driver extends BaseEntity { + @Column({ name: 'license_number', unique: true }) + licenseNumber!: string; + + @Column({ name: 'first_name' }) + firstName!: string; + + @Column({ name: 'last_name' }) + lastName!: string; + + @Column({ unique: true }) + email!: string; + + @Column({ name: 'phone_number', unique: true }) + phoneNumber!: string; + + @Column({ name: 'date_of_birth', type: 'date' }) + dateOfBirth!: Date; + + @Column({ name: 'license_expiry_date', type: 'date' }) + licenseExpiryDate!: Date; + + @Column({ type: 'varchar', default: DriverStatus.ACTIVE }) + status!: DriverStatus; + + @Column({ name: 'vehicle_types_authorized', type: 'varchar', array: true, nullable: true }) + vehicleTypesAuthorized?: string[]; + + @Column({ type: 'text', nullable: true }) + address!: string | null; + + @Column({ name: 'emergency_contact', nullable: true }) + emergencyContact!: string | null; + + @Column({ type: 'text', nullable: true }) + notes!: string | null; + + @Column({ name: 'total_trips', type: 'int', default: 0 }) + totalTrips!: number; + + @Column({ type: 'numeric', precision: 3, scale: 2, nullable: true }) + rating!: number | null; +} diff --git a/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts b/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts new file mode 100644 index 000000000..fb5c4e92b --- /dev/null +++ b/apps/edr-freight-api/src/modules/vehicles/dto/create-vehicle.dto.ts @@ -0,0 +1,32 @@ +import { IsString, IsEnum, IsNumber, IsOptional } from 'class-validator'; +import { VehicleType, FuelType, VehicleStatus } from '../entities/vehicle.entity'; + +export class CreateVehicleDto { + @IsString() + plateNumber!: string; + + @IsEnum(VehicleType) + vehicleType!: VehicleType; + + @IsString() + manufacturer!: string; + + @IsString() + model!: string; + + @IsNumber() + year!: number; + + @IsEnum(FuelType) + fuelType!: FuelType; + + @IsNumber() + capacity!: number; + + @IsEnum(VehicleStatus) + status!: VehicleStatus; + + @IsOptional() + @IsString() + description?: string; +} diff --git a/apps/edr-freight-api/src/modules/vehicles/dto/update-vehicle.dto.ts b/apps/edr-freight-api/src/modules/vehicles/dto/update-vehicle.dto.ts new file mode 100644 index 000000000..953917b2f --- /dev/null +++ b/apps/edr-freight-api/src/modules/vehicles/dto/update-vehicle.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateVehicleDto } from './create-vehicle.dto'; + +export class UpdateVehicleDto extends PartialType(CreateVehicleDto) {} diff --git a/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts b/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts new file mode 100644 index 000000000..773e8051a --- /dev/null +++ b/apps/edr-freight-api/src/modules/vehicles/entities/vehicle.entity.ts @@ -0,0 +1,64 @@ +import { Entity, Column, Index } from 'typeorm'; +import { BaseEntity } from '@edr/api-common'; + +export enum VehicleType { + TRUCK = 'TRUCK', + VAN = 'VAN', + CAR = 'CAR', + BUS = 'BUS', + TRAILER = 'TRAILER', + TANKER = 'TANKER', + FLATBED = 'FLATBED', +} + +export enum FuelType { + PETROL = 'PETROL', + DIESEL = 'DIESEL', + ELECTRIC = 'ELECTRIC', + HYBRID = 'HYBRID', +} + +export enum VehicleStatus { + ACTIVE = 'ACTIVE', + MAINTENANCE = 'MAINTENANCE', + RETIRED = 'RETIRED', + OUT_OF_SERVICE = 'OUT_OF_SERVICE', +} + +@Entity({ name: 'vehicles', schema: 'freight' }) +@Index(['plateNumber']) +@Index(['registrationNumber']) +@Index(['status']) +@Index(['vehicleType']) +@Index(['manufacturer']) +export class Vehicle extends BaseEntity { + @Column({ name: 'plate_number', unique: true }) + plateNumber!: string; + + @Column({ name: 'registration_number', unique: true }) + registrationNumber!: string; + + @Column({ name: 'vehicle_type', type: 'varchar' }) + vehicleType!: VehicleType; + + @Column() + manufacturer!: string; + + @Column() + model!: string; + + @Column() + year!: number; + + @Column({ name: 'fuel_type', type: 'varchar' }) + fuelType!: FuelType; + + @Column() + capacity!: number; + + @Column({ name: 'status', type: 'varchar', default: VehicleStatus.ACTIVE }) + status!: VehicleStatus; + + @Column({ type: 'text', nullable: true }) + description!: string | null; +} diff --git a/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts b/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts new file mode 100644 index 000000000..24ff2d022 --- /dev/null +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.controller.ts @@ -0,0 +1,74 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Param, + Body, + Query, + ParseUUIDPipe, +} from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FleetManage, FleetView } from '../../common/booking-guards'; +import { VehiclesService } from './vehicles.service'; +import { CreateVehicleDto } from './dto/create-vehicle.dto'; +import { UpdateVehicleDto } from './dto/update-vehicle.dto'; + +@ApiTags('vehicles') +@ApiBearerAuth() +@Controller('vehicles') +@FleetView() +export class VehiclesController { + constructor(private readonly vehiclesService: VehiclesService) {} + + @Post() + @FleetManage() + @ApiOperation({ summary: 'Create a new vehicle' }) + create(@Body() createVehicleDto: CreateVehicleDto) { + return this.vehiclesService.create(createVehicleDto); + } + + @Get() + @ApiOperation({ summary: 'Get all vehicles with filters' }) + findAll( + @Query('search') search?: string, + @Query('status') status?: string, + @Query('page') page?: string, + @Query('limit') limit?: string, + @Query('sortBy') sortBy?: string, + @Query('sortOrder') sortOrder?: 'ASC' | 'DESC', + ) { + return this.vehiclesService.findAll({ + search, + status: status as any, + page: page ? parseInt(page) : undefined, + limit: limit ? parseInt(limit) : undefined, + sortBy, + sortOrder, + }); + } + + @Get(':id') + @ApiOperation({ summary: 'Get vehicle by id' }) + findOne(@Param('id', ParseUUIDPipe) id: string) { + return this.vehiclesService.findById(id); + } + + @Patch(':id') + @FleetManage() + @ApiOperation({ summary: 'Update a vehicle' }) + update( + @Param('id', ParseUUIDPipe) id: string, + @Body() updateVehicleDto: UpdateVehicleDto, + ) { + return this.vehiclesService.update(id, updateVehicleDto); + } + + @Delete(':id') + @FleetManage() + @ApiOperation({ summary: 'Delete a vehicle' }) + remove(@Param('id', ParseUUIDPipe) id: string) { + return this.vehiclesService.remove(id); + } +} diff --git a/apps/edr-freight-api/src/modules/vehicles/vehicles.module.ts b/apps/edr-freight-api/src/modules/vehicles/vehicles.module.ts new file mode 100644 index 000000000..07aa4bd2f --- /dev/null +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Vehicle } from './entities/vehicle.entity'; +import { VehiclesService } from './vehicles.service'; +import { VehiclesController } from './vehicles.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Vehicle])], + providers: [VehiclesService], + controllers: [VehiclesController], + exports: [VehiclesService], +}) +export class VehiclesModule {} diff --git a/apps/edr-freight-api/src/modules/vehicles/vehicles.repository.ts b/apps/edr-freight-api/src/modules/vehicles/vehicles.repository.ts index 46950e0c9..f7b0ae8d6 100644 --- a/apps/edr-freight-api/src/modules/vehicles/vehicles.repository.ts +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.repository.ts @@ -69,8 +69,7 @@ export class VehiclesRepository extends BaseRepository { async createVehicle(vehicleData: any): Promise { const vehicle = this.repository.create(vehicleData); - const vehicles = await this.repository.save(vehicle); - return vehicles?.[0]; + return await this.repository.save(vehicle); } async updateVehicle(vehicle: Vehicle): Promise { diff --git a/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts new file mode 100644 index 000000000..448e2ff91 --- /dev/null +++ b/apps/edr-freight-api/src/modules/vehicles/vehicles.service.ts @@ -0,0 +1,106 @@ +import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateVehicleDto } from './dto/create-vehicle.dto'; +import { UpdateVehicleDto } from './dto/update-vehicle.dto'; +import { Vehicle, VehicleStatus } from './entities/vehicle.entity'; + +@Injectable() +export class VehiclesService { + constructor( + @InjectRepository(Vehicle) + private readonly vehicleRepo: Repository, + ) {} + + async create(dto: CreateVehicleDto): Promise { + const existing = await this.vehicleRepo.findOne({ + where: { plateNumber: dto.plateNumber }, + }); + + if (existing) { + throw new ConflictException( + `Vehicle with plate number ${dto.plateNumber} already exists`, + ); + } + + const registrationNumber = `REG-${dto.vehicleType}-${Date.now()}`; + const vehicle = this.vehicleRepo.create({ + ...dto, + registrationNumber, + }); + + return this.vehicleRepo.save(vehicle); + } + + async findAll(query: { + search?: string; + status?: VehicleStatus | string; + page?: number; + limit?: number; + sortBy?: string; + sortOrder?: 'ASC' | 'DESC'; + } = {}): Promise<{ data: Vehicle[]; total: number; page: number; limit: number }> { + const page = query.page || 1; + const limit = query.limit || 10; + const skip = (page - 1) * limit; + + let qb = this.vehicleRepo.createQueryBuilder('v'); + + if (query.search) { + qb = qb.where( + 'v.plateNumber ILIKE :search OR v.manufacturer ILIKE :search', + { search: `%${query.search}%` }, + ); + } + + if (query.status) { + qb = qb.andWhere('v.status = :status', { status: query.status }); + } + + const sortBy = ['plateNumber', 'status', 'year', 'createdAt'].includes( + query.sortBy ?? '', + ) + ? query.sortBy + : 'createdAt'; + const sortOrder = (query.sortOrder ?? 'DESC').toUpperCase(); + + const [data, total] = await qb + .orderBy(`v.${sortBy}`, sortOrder as 'ASC' | 'DESC') + .skip(skip) + .take(limit) + .getManyAndCount(); + + return { data, total, page, limit }; + } + + async findById(id: string): Promise { + const vehicle = await this.vehicleRepo.findOne({ where: { id } }); + if (!vehicle) { + throw new NotFoundException(`Vehicle ${id} not found`); + } + return vehicle; + } + + async update(id: string, dto: UpdateVehicleDto): Promise { + const vehicle = await this.findById(id); + + if (dto.plateNumber && dto.plateNumber !== vehicle.plateNumber) { + const existing = await this.vehicleRepo.findOne({ + where: { plateNumber: dto.plateNumber }, + }); + if (existing) { + throw new ConflictException( + `Vehicle with plate number ${dto.plateNumber} already exists`, + ); + } + } + + Object.assign(vehicle, dto); + return this.vehicleRepo.save(vehicle); + } + + async remove(id: string): Promise { + await this.findById(id); + await this.vehicleRepo.softDelete(id); + } +}