Backend: - Extract shared hard-delete authorization utility (#9) - Extract Prisma include constants per entity (#11) - Fix N+1 query pattern in events findAll (#12) - Extract shared date utility functions (#13) - Move vehicle utilization filtering to DB query (#15) - Add ParseBooleanPipe for query params - Add CurrentDriver decorator + ResolveDriverInterceptor (#20) Frontend: - Extract shared form utilities (toDatetimeLocal) and enum labels (#17) - Replace browser confirm() with styled ConfirmModal (#18) - Add centralized query-keys.ts constants (#19) - Clean up unused imports, add useMemo where needed (#19) - Standardize filter button styling across list pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
3.6 KiB
TypeScript
136 lines
3.6 KiB
TypeScript
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { CreateVehicleDto, UpdateVehicleDto } from './dto';
|
|
import { executeHardDelete } from '../common/utils';
|
|
|
|
@Injectable()
|
|
export class VehiclesService {
|
|
private readonly logger = new Logger(VehiclesService.name);
|
|
|
|
private readonly vehicleInclude = {
|
|
currentDriver: true,
|
|
events: {
|
|
where: { deletedAt: null },
|
|
include: { driver: true, vehicle: true },
|
|
orderBy: { startTime: 'asc' as const },
|
|
},
|
|
} as const;
|
|
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
async create(createVehicleDto: CreateVehicleDto) {
|
|
this.logger.log(`Creating vehicle: ${createVehicleDto.name}`);
|
|
|
|
return this.prisma.vehicle.create({
|
|
data: createVehicleDto,
|
|
include: this.vehicleInclude,
|
|
});
|
|
}
|
|
|
|
async findAll() {
|
|
return this.prisma.vehicle.findMany({
|
|
where: { deletedAt: null },
|
|
include: this.vehicleInclude,
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
}
|
|
|
|
async findAvailable() {
|
|
return this.prisma.vehicle.findMany({
|
|
where: {
|
|
deletedAt: null,
|
|
status: 'AVAILABLE',
|
|
},
|
|
include: {
|
|
currentDriver: true,
|
|
},
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
}
|
|
|
|
async findOne(id: string) {
|
|
const vehicle = await this.prisma.vehicle.findFirst({
|
|
where: { id, deletedAt: null },
|
|
include: this.vehicleInclude,
|
|
});
|
|
|
|
if (!vehicle) {
|
|
throw new NotFoundException(`Vehicle with ID ${id} not found`);
|
|
}
|
|
|
|
return vehicle;
|
|
}
|
|
|
|
async update(id: string, updateVehicleDto: UpdateVehicleDto) {
|
|
const vehicle = await this.findOne(id);
|
|
|
|
this.logger.log(`Updating vehicle ${id}: ${vehicle.name}`);
|
|
|
|
return this.prisma.vehicle.update({
|
|
where: { id: vehicle.id },
|
|
data: updateVehicleDto,
|
|
include: this.vehicleInclude,
|
|
});
|
|
}
|
|
|
|
async remove(id: string, hardDelete = false, userRole?: string) {
|
|
return executeHardDelete({
|
|
id,
|
|
hardDelete,
|
|
userRole,
|
|
findOne: (id) => this.findOne(id),
|
|
performHardDelete: (id) => this.prisma.vehicle.delete({ where: { id } }),
|
|
performSoftDelete: (id) =>
|
|
this.prisma.vehicle.update({
|
|
where: { id },
|
|
data: { deletedAt: new Date() },
|
|
}),
|
|
entityName: 'Vehicle',
|
|
logger: this.logger,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get vehicle utilization statistics
|
|
*/
|
|
async getUtilization() {
|
|
const now = new Date();
|
|
|
|
// Fetch vehicles with only upcoming events (filtered at database level)
|
|
const vehicles = await this.prisma.vehicle.findMany({
|
|
where: { deletedAt: null },
|
|
include: {
|
|
currentDriver: true,
|
|
events: {
|
|
where: {
|
|
deletedAt: null,
|
|
startTime: { gt: now }, // Only fetch upcoming events
|
|
},
|
|
include: { driver: true, vehicle: true },
|
|
orderBy: { startTime: 'asc' },
|
|
},
|
|
},
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
|
|
const stats = vehicles.map((vehicle) => ({
|
|
id: vehicle.id,
|
|
name: vehicle.name,
|
|
type: vehicle.type,
|
|
seatCapacity: vehicle.seatCapacity,
|
|
status: vehicle.status,
|
|
upcomingTrips: vehicle.events.length, // Already filtered at DB level
|
|
currentDriver: vehicle.currentDriver?.name,
|
|
}));
|
|
|
|
return {
|
|
totalVehicles: vehicles.length,
|
|
available: vehicles.filter((v) => v.status === 'AVAILABLE').length,
|
|
inUse: vehicles.filter((v) => v.status === 'IN_USE').length,
|
|
maintenance: vehicles.filter((v) => v.status === 'MAINTENANCE').length,
|
|
reserved: vehicles.filter((v) => v.status === 'RESERVED').length,
|
|
vehicles: stats,
|
|
};
|
|
}
|
|
}
|