mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 05:30:55 +00:00
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
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<MaintenanceSchedule> {
|
|
constructor(
|
|
@InjectRepository(MaintenanceSchedule)
|
|
private readonly scheduleRepository: Repository<MaintenanceSchedule>,
|
|
@InjectRepository(MaintenanceCost)
|
|
private readonly costRepository: Repository<MaintenanceCost>,
|
|
) {
|
|
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)
|
|
)
|
|
`);
|
|
}
|
|
}
|