feat: complete maintenance tracking backend

Service/Controller/Module:
- scheduleMaintenanceAsync: schedule work
- recordMaintenanceCost: log expenses
- getUpcomingMaintenance: due items
- getVehicleMaintenanceStats: cost aggregation

Endpoints:
- POST /maintenance/schedules
- POST /maintenance/costs
- PATCH /maintenance/schedules/:id
- GET /maintenance/upcoming/:vehicleId
- GET /maintenance/history/:vehicleId
- GET /maintenance/stats/:vehicleId

Migration: idempotent maintenance tables creation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
natib21
2026-06-30 12:32:08 +00:00
parent e59a77b859
commit f436916c42
5 changed files with 227 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
import { Controller, Post, Get, Patch, Body, Param } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { MaintenanceService } from './maintenance.service';
import { CreateMaintenanceScheduleDto, CreateMaintenanceCostDto, UpdateMaintenanceScheduleDto } from './dto/create-maintenance.dto';
@ApiTags('Maintenance Management')
@Controller('maintenance')
export class MaintenanceController {
constructor(private readonly maintenanceService: MaintenanceService) {}
@Post('schedules')
@ApiOperation({ summary: 'Schedule maintenance' })
async scheduleMaintenanceAsync(@Body() dto: CreateMaintenanceScheduleDto) {
return this.maintenanceService.scheduleMaintenanceAsync(dto);
}
@Post('costs')
@ApiOperation({ summary: 'Record maintenance cost' })
async recordCost(@Body() dto: CreateMaintenanceCostDto) {
return this.maintenanceService.recordMaintenanceCost(dto);
}
@Patch('schedules/:id')
@ApiOperation({ summary: 'Update maintenance schedule' })
async updateSchedule(@Param('id') id: string, @Body() dto: UpdateMaintenanceScheduleDto) {
return this.maintenanceService.updateMaintenanceSchedule(id, dto);
}
@Get('upcoming/:vehicleId')
@ApiOperation({ summary: 'Get upcoming maintenance' })
async getUpcoming(@Param('vehicleId') vehicleId: string) {
return this.maintenanceService.getUpcomingMaintenance(vehicleId);
}
@Get('history/:vehicleId')
@ApiOperation({ summary: 'Get maintenance history' })
async getHistory(@Param('vehicleId') vehicleId: string) {
return this.maintenanceService.getMaintenanceHistory(vehicleId);
}
@Get('stats/:vehicleId')
@ApiOperation({ summary: 'Get maintenance statistics' })
async getStats(@Param('vehicleId') vehicleId: string) {
return this.maintenanceService.getVehicleMaintenanceStats(vehicleId);
}
}

View File

@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MaintenanceSchedule } from './entities/maintenance-schedule.entity';
import { MaintenanceCost } from './entities/maintenance-cost.entity';
import { MaintenanceService } from './maintenance.service';
import { MaintenanceRepository } from './maintenance.repository';
import { MaintenanceController } from './maintenance.controller';
@Module({
imports: [TypeOrmModule.forFeature([MaintenanceSchedule, MaintenanceCost])],
providers: [MaintenanceService, MaintenanceRepository],
controllers: [MaintenanceController],
exports: [MaintenanceService],
})
export class MaintenanceModule {}

View File

@@ -0,0 +1,82 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { MaintenanceRepository } from './maintenance.repository';
import { MaintenanceSchedule } from './entities/maintenance-schedule.entity';
import { MaintenanceCost } from './entities/maintenance-cost.entity';
import { CreateMaintenanceScheduleDto, CreateMaintenanceCostDto, UpdateMaintenanceScheduleDto } from './dto/create-maintenance.dto';
@Injectable()
export class MaintenanceService {
constructor(
private readonly maintenanceRepository: MaintenanceRepository,
@InjectRepository(MaintenanceSchedule)
private readonly scheduleRepository: Repository<MaintenanceSchedule>,
@InjectRepository(MaintenanceCost)
private readonly costRepository: Repository<MaintenanceCost>,
) {}
async scheduleMaintenanceAsync(dto: CreateMaintenanceScheduleDto): Promise<MaintenanceSchedule> {
const schedule = this.scheduleRepository.create({
...dto,
scheduledDate: new Date(dto.scheduledDate),
nextDueDate: dto.nextDueDate ? new Date(dto.nextDueDate) : undefined,
});
return this.scheduleRepository.save(schedule);
}
async recordMaintenanceCost(dto: CreateMaintenanceCostDto): Promise<MaintenanceCost> {
const cost = this.costRepository.create({
...dto,
incurredDate: new Date(dto.incurredDate),
});
return this.costRepository.save(cost);
}
async updateMaintenanceSchedule(
id: string,
dto: UpdateMaintenanceScheduleDto,
): Promise<MaintenanceSchedule> {
await this.scheduleRepository.update(id, {
...dto,
completedDate: dto.completedDate ? new Date(dto.completedDate) : undefined,
});
const updated = await this.scheduleRepository.findOneBy({ id });
return updated!;
}
async getUpcomingMaintenance(vehicleId: string) {
return this.maintenanceRepository.getUpcomingMaintenance(vehicleId);
}
async getMaintenanceHistory(vehicleId: string, monthsBack: number = 12) {
const endDate = new Date();
const startDate = new Date(endDate.getFullYear(), endDate.getMonth() - monthsBack, 1);
return this.maintenanceRepository.getMaintenanceCosts(vehicleId, startDate, endDate);
}
async getVehicleMaintenanceStats(vehicleId: string, monthsBack: number = 12) {
const endDate = new Date();
const startDate = new Date(endDate.getFullYear(), endDate.getMonth() - monthsBack, 1);
const costs = await this.maintenanceRepository.getMaintenanceCosts(vehicleId, startDate, endDate);
const totalCost = costs.reduce((sum: number, c: MaintenanceCost) => sum + Number(c.costAmount), 0);
return {
vehicleId,
totalCost,
numberOfMaintenanceItems: costs.length,
averageCostPerMaintenance: costs.length > 0 ? totalCost / costs.length : 0,
costByType: this.groupCostsByType(costs),
};
}
private groupCostsByType(costs: MaintenanceCost[]) {
const grouped: Record<string, number> = {};
costs.forEach((c) => {
if (!grouped[c.costType]) grouped[c.costType] = 0;
grouped[c.costType] += Number(c.costAmount);
});
return grouped;
}
}