feat: implement KM-based maintenance scheduling

Add maintenance intervals configuration to track maintenance by kilometers
driven. When a maintenance is marked COMPLETED, automatically calculate and
schedule the next maintenance based on interval + current odometer reading.

Features:
- MaintenanceInterval entity: stores KM/day intervals per vehicle & type
- scheduleNextMaintenance(): creates next SCHEDULED item after completion
- nextDueKm field: tracks when next maintenance is due (in kilometers)
- getDueBoard() queries already support KM-based tracking

Maintenance now "marches forward" based on distance driven, not just dates.
Each vehicle type can have different intervals (e.g., oil every 10k km, tires 50k km).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Hagernesh
2026-07-23 07:04:40 +00:00
parent b756ae2f1c
commit fa59ccfc95
5 changed files with 275 additions and 2 deletions

View File

@@ -4,7 +4,8 @@ import { Cron, CronExpression } from '@nestjs/schedule';
import { NotificationAudience, NotificationType } from '@edr/types';
import { DataSource, Repository } from 'typeorm';
import { MaintenanceRepository } from './maintenance.repository';
import { MaintenanceSchedule, MaintenanceStatus } from './entities/maintenance-schedule.entity';
import { MaintenanceIntervalRepository } from './maintenance-interval.repository';
import { MaintenanceSchedule, MaintenanceStatus, MaintenanceType } from './entities/maintenance-schedule.entity';
import { MaintenanceCost } from './entities/maintenance-cost.entity';
import { Vehicle, VehicleAvailability, VehicleStatus } from '../vehicles/entities/vehicle.entity';
import { CreateMaintenanceScheduleDto, CreateMaintenanceCostDto, UpdateMaintenanceScheduleDto } from './dto/create-maintenance.dto';
@@ -16,6 +17,7 @@ export class MaintenanceService {
constructor(
private readonly maintenanceRepository: MaintenanceRepository,
private readonly intervalRepository: MaintenanceIntervalRepository,
@InjectRepository(MaintenanceSchedule)
private readonly scheduleRepository: Repository<MaintenanceSchedule>,
@InjectRepository(MaintenanceCost)
@@ -119,6 +121,11 @@ export class MaintenanceService {
) {
// Maintenance finished/aborted → vehicle back in service.
await this.setVehicleMaintenanceState(updated.vehicleId, false);
// If completed, schedule the next maintenance based on interval
if (dto.status === MaintenanceStatus.COMPLETED && updated.odometerReading != null) {
await this.scheduleNextMaintenance(updated);
}
} else if (dto.status === MaintenanceStatus.IN_PROGRESS) {
// Maintenance started → keep the vehicle out of service.
await this.setVehicleMaintenanceState(updated.vehicleId, true);
@@ -128,6 +135,60 @@ export class MaintenanceService {
return updated!;
}
private async scheduleNextMaintenance(completed: MaintenanceSchedule): Promise<void> {
try {
// Get maintenance interval for this type
const interval = await this.intervalRepository.getByVehicleAndType(
completed.vehicleId,
completed.maintenanceType as MaintenanceType,
);
if (!interval) return; // No interval defined, skip auto-scheduling
const now = new Date();
const completedKm = Number(completed.odometerReading ?? 0);
// Calculate next due based on KM interval
if (interval.intervalKm && interval.intervalKm > 0) {
const nextDueKm = completedKm + Number(interval.intervalKm);
// Create next scheduled maintenance
const nextSchedule = this.scheduleRepository.create({
vehicleId: completed.vehicleId,
maintenanceType: completed.maintenanceType,
description: `${interval.description || completed.description} (Next interval: ${nextDueKm} km)`,
scheduledDate: now,
nextDueKm,
status: MaintenanceStatus.SCHEDULED,
});
await this.scheduleRepository.save(nextSchedule);
}
// Calculate next due based on date interval
if (interval.intervalDays && interval.intervalDays > 0) {
const nextDueDate = new Date(now.getTime() + interval.intervalDays * 24 * 60 * 60 * 1000);
// If no KM-based next maintenance was created, use date-based
if (!interval.intervalKm) {
const nextSchedule = this.scheduleRepository.create({
vehicleId: completed.vehicleId,
maintenanceType: completed.maintenanceType,
description: completed.description,
scheduledDate: now,
nextDueDate,
status: MaintenanceStatus.SCHEDULED,
});
await this.scheduleRepository.save(nextSchedule);
}
}
} catch (err) {
this.logger.error(
`Failed to schedule next maintenance for vehicle ${completed.vehicleId}: ${(err as Error).message}`,
(err as Error).stack,
);
}
}
async getUpcomingMaintenance(vehicleId: string) {
return this.maintenanceRepository.getUpcomingMaintenance(vehicleId);
}