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,82 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateMaintenanceTables1850000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Create maintenance_schedules table
const scheduleTableExists = await queryRunner.query(`
SELECT EXISTS(
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'freight' AND table_name = 'maintenance_schedules'
)
`);
if (!scheduleTableExists[0].exists) {
await queryRunner.query(`
CREATE TABLE "freight"."maintenance_schedules" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"vehicle_id" uuid NOT NULL,
"maintenance_type" varchar NOT NULL,
"description" varchar NOT NULL,
"scheduled_date" timestamptz NOT NULL,
"completed_date" timestamptz,
"estimated_cost" numeric(14,2),
"actual_cost" numeric(14,2),
"status" varchar NOT NULL DEFAULT 'SCHEDULED',
"odometer_reading" numeric,
"service_provider" varchar,
"notes" text,
"next_due_km" numeric,
"next_due_date" timestamptz,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
"deleted_at" timestamptz,
PRIMARY KEY ("id")
)
`);
await queryRunner.query(
`CREATE INDEX "idx_maintenance_schedules_vehicle_date" ON "freight"."maintenance_schedules" ("vehicle_id", "scheduled_date")`
);
}
// Create maintenance_costs table
const costsTableExists = await queryRunner.query(`
SELECT EXISTS(
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'freight' AND table_name = 'maintenance_costs'
)
`);
if (!costsTableExists[0].exists) {
await queryRunner.query(`
CREATE TABLE "freight"."maintenance_costs" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"vehicle_id" uuid NOT NULL,
"maintenance_schedule_id" uuid,
"incurred_date" timestamptz NOT NULL,
"cost_amount" numeric(14,2) NOT NULL,
"cost_type" varchar NOT NULL,
"description" varchar NOT NULL,
"service_provider" varchar,
"invoice_number" varchar,
"notes" text,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
"deleted_at" timestamptz,
PRIMARY KEY ("id"),
CONSTRAINT "fk_maintenance_schedule" FOREIGN KEY ("maintenance_schedule_id")
REFERENCES "freight"."maintenance_schedules" ("id") ON DELETE SET NULL
)
`);
await queryRunner.query(
`CREATE INDEX "idx_maintenance_costs_vehicle_date" ON "freight"."maintenance_costs" ("vehicle_id", "incurred_date")`
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "freight"."maintenance_costs"`);
await queryRunner.query(`DROP TABLE IF EXISTS "freight"."maintenance_schedules"`);
}
}