import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { BaseRepository } from '@edr/api-common'; import { Repository, Between } from 'typeorm'; import { MaintenanceSchedule, MaintenanceStatus } from './entities/maintenance-schedule.entity'; import { MaintenanceCost } from './entities/maintenance-cost.entity'; @Injectable() export class MaintenanceRepository extends BaseRepository { constructor( @InjectRepository(MaintenanceSchedule) private readonly scheduleRepository: Repository, @InjectRepository(MaintenanceCost) private readonly costRepository: Repository, ) { super(scheduleRepository); } async getUpcomingMaintenance(vehicleId: string, daysAhead: number = 30) { const futureDate = new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000); return this.scheduleRepository.find({ where: { vehicleId, scheduledDate: Between(new Date(), futureDate), status: MaintenanceStatus.SCHEDULED, }, order: { scheduledDate: 'ASC' }, }); } async getMaintenanceCosts(vehicleId: string, startDate: Date, endDate: Date) { return this.costRepository.find({ where: { vehicleId, incurredDate: Between(startDate, endDate), }, order: { incurredDate: 'DESC' }, }); } async getTotalMaintenanceCost(vehicleId: string, startDate: Date, endDate: Date) { const result = await this.costRepository .createQueryBuilder() .select('SUM(cost_amount)', 'total') .where('vehicle_id = :vehicleId', { vehicleId }) .andWhere('incurred_date BETWEEN :startDate AND :endDate', { startDate, endDate }) .getRawOne(); return result?.total || 0; } /** * Fleet-wide "next due" board: one row per vehicle with a SCHEDULED * maintenance item, driven by time AND km — whichever is soonest. Current km * is the vehicle's latest fuel-up odometer reading (how mileage is actually * captured today), falling back to vehicle.actual_distance_km when the * vehicle has no fuel purchase on file yet. */ async getDueBoard(): Promise< Array<{ scheduleId: string; vehicleId: string; plateNumber: string; maintenanceType: string; serviceItem: string | null; description: string; scheduledDate: Date; nextDueDate: Date | null; nextDueKm: number | null; currentKm: number | null; kmRemaining: number | null; daysRemaining: number | null; overdue: boolean; }> > { // Every SCHEDULED item, not one per vehicle — a truck legitimately holds // several (oil vs tires intervals differ). return this.scheduleRepository.manager.query(` SELECT s.id AS "scheduleId", s.vehicle_id AS "vehicleId", v.plate_number AS "plateNumber", s.maintenance_type AS "maintenanceType", s.service_item AS "serviceItem", s.description, s.scheduled_date AS "scheduledDate", s.next_due_date AS "nextDueDate", s.next_due_km AS "nextDueKm", COALESCE(fp.max_odometer, v.actual_distance_km) AS "currentKm", CASE WHEN s.next_due_km IS NOT NULL THEN s.next_due_km - COALESCE(fp.max_odometer, v.actual_distance_km, 0) ELSE NULL END AS "kmRemaining", CASE WHEN s.next_due_date IS NOT NULL THEN EXTRACT(DAY FROM s.next_due_date - now()) ELSE NULL END AS "daysRemaining", ( (s.next_due_date IS NOT NULL AND s.next_due_date <= now()) OR (s.next_due_km IS NOT NULL AND COALESCE(fp.max_odometer, v.actual_distance_km, 0) >= s.next_due_km) ) AS overdue FROM freight.maintenance_schedules s JOIN freight.vehicles v ON v.id = s.vehicle_id AND v.deleted_at IS NULL LEFT JOIN LATERAL ( SELECT MAX(odometer_reading) AS max_odometer FROM freight.fuel_purchases fp2 WHERE fp2.vehicle_id = s.vehicle_id ) fp ON true WHERE s.status = 'SCHEDULED' AND s.deleted_at IS NULL ORDER BY s.vehicle_id, s.scheduled_date ASC `); } /** * SCHEDULED items that have crossed their km or date due-point and have not * yet been notified. Backs the daily km/date maintenance alert. */ async getUnnotifiedDue(): Promise< Array<{ id: string; vehicleId: string; plateNumber: string; maintenanceType: string; description: string; nextDueKm: number | null; nextDueDate: Date | null; currentKm: number | null; }> > { return this.scheduleRepository.manager.query(` SELECT s.id, s.vehicle_id AS "vehicleId", v.plate_number AS "plateNumber", s.maintenance_type AS "maintenanceType", s.description, s.next_due_km AS "nextDueKm", s.next_due_date AS "nextDueDate", COALESCE(fp.max_odometer, v.actual_distance_km) AS "currentKm" FROM freight.maintenance_schedules s JOIN freight.vehicles v ON v.id = s.vehicle_id AND v.deleted_at IS NULL LEFT JOIN LATERAL ( SELECT MAX(odometer_reading) AS max_odometer FROM freight.fuel_purchases fp2 WHERE fp2.vehicle_id = s.vehicle_id ) fp ON true WHERE s.status = 'SCHEDULED' AND s.deleted_at IS NULL AND s.due_notified_at IS NULL AND ( (s.next_due_date IS NOT NULL AND s.next_due_date <= now()) OR (s.next_due_km IS NOT NULL AND COALESCE(fp.max_odometer, v.actual_distance_km, 0) >= s.next_due_km) ) `); } }