1038 lines
32 KiB
Markdown
1038 lines
32 KiB
Markdown
# Maintenance Feature Implementation Plan
|
|
|
|
## Project Context
|
|
|
|
MotoVaultPro is a hybrid platform using:
|
|
- **Architecture**: Modular monolith with feature capsules
|
|
- **Backend**: Fastify + PostgreSQL + Redis
|
|
- **Frontend**: React + TypeScript + Material-UI
|
|
- **Development**: Docker-first, production-only testing
|
|
- **Requirements**: Mobile + Desktop support for ALL features
|
|
|
|
## Feature Overview
|
|
|
|
Implement comprehensive maintenance tracking with three main categories:
|
|
1. **Routine Maintenance** - Regular service items (27 subtypes)
|
|
2. **Repair** - Fix/repair work (5 subtypes)
|
|
3. **Performance Upgrade** - Enhancements (5 subtypes)
|
|
|
|
### Key Capabilities
|
|
- Track completed maintenance (records)
|
|
- Schedule recurring maintenance (schedules)
|
|
- Calculate next due dates (date-based and/or mileage-based)
|
|
- View upcoming/overdue maintenance
|
|
- Support multiple subtypes per record (checkboxes, not single select)
|
|
|
|
### Display Format
|
|
- **List View**: "Category (count)" e.g., "Routine Maintenance (3)"
|
|
- **Detail View**: Show all selected subtypes with full details
|
|
|
|
## Database Schema
|
|
|
|
### Tables to Create
|
|
|
|
Drop existing maintenance tables (001_create_maintenance_tables.sql) and create new schema.
|
|
|
|
**Migration: `backend/src/features/maintenance/migrations/002_recreate_maintenance_tables.sql`**
|
|
|
|
```sql
|
|
-- Drop existing tables (clean slate)
|
|
DROP TABLE IF EXISTS maintenance_schedules CASCADE;
|
|
DROP TABLE IF EXISTS maintenance_logs CASCADE;
|
|
|
|
-- Create maintenance_records table
|
|
CREATE TABLE maintenance_records (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id VARCHAR(255) NOT NULL,
|
|
vehicle_id UUID NOT NULL,
|
|
category VARCHAR(50) NOT NULL, -- 'routine_maintenance', 'repair', 'performance_upgrade'
|
|
subtypes TEXT[] NOT NULL, -- PostgreSQL array of selected subtypes
|
|
date DATE NOT NULL,
|
|
odometer_reading INTEGER,
|
|
cost DECIMAL(10, 2),
|
|
shop_name VARCHAR(200),
|
|
notes TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT fk_maintenance_vehicle
|
|
FOREIGN KEY (vehicle_id)
|
|
REFERENCES vehicles(id)
|
|
ON DELETE CASCADE,
|
|
|
|
CONSTRAINT check_category
|
|
CHECK (category IN ('routine_maintenance', 'repair', 'performance_upgrade')),
|
|
|
|
CONSTRAINT check_subtypes_not_empty
|
|
CHECK (array_length(subtypes, 1) > 0)
|
|
);
|
|
|
|
-- Create maintenance_schedules table
|
|
CREATE TABLE maintenance_schedules (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id VARCHAR(255) NOT NULL,
|
|
vehicle_id UUID NOT NULL,
|
|
category VARCHAR(50) NOT NULL,
|
|
subtypes TEXT[] NOT NULL,
|
|
interval_months INTEGER,
|
|
interval_miles INTEGER,
|
|
last_service_date DATE,
|
|
last_service_mileage INTEGER,
|
|
next_due_date DATE,
|
|
next_due_mileage INTEGER,
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT fk_schedule_vehicle
|
|
FOREIGN KEY (vehicle_id)
|
|
REFERENCES vehicles(id)
|
|
ON DELETE CASCADE,
|
|
|
|
CONSTRAINT check_schedule_category
|
|
CHECK (category IN ('routine_maintenance', 'repair', 'performance_upgrade'))
|
|
);
|
|
|
|
-- Indexes for performance
|
|
CREATE INDEX idx_maintenance_records_user_id ON maintenance_records(user_id);
|
|
CREATE INDEX idx_maintenance_records_vehicle_id ON maintenance_records(vehicle_id);
|
|
CREATE INDEX idx_maintenance_records_date ON maintenance_records(date DESC);
|
|
CREATE INDEX idx_maintenance_records_category ON maintenance_records(category);
|
|
|
|
CREATE INDEX idx_maintenance_schedules_user_id ON maintenance_schedules(user_id);
|
|
CREATE INDEX idx_maintenance_schedules_vehicle_id ON maintenance_schedules(vehicle_id);
|
|
CREATE INDEX idx_maintenance_schedules_next_due_date ON maintenance_schedules(next_due_date);
|
|
CREATE INDEX idx_maintenance_schedules_active ON maintenance_schedules(is_active) WHERE is_active = true;
|
|
|
|
-- Triggers for updated_at
|
|
DROP TRIGGER IF EXISTS update_maintenance_records_updated_at ON maintenance_records;
|
|
CREATE TRIGGER update_maintenance_records_updated_at
|
|
BEFORE UPDATE ON maintenance_records
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
DROP TRIGGER IF EXISTS update_maintenance_schedules_updated_at ON maintenance_schedules;
|
|
CREATE TRIGGER update_maintenance_schedules_updated_at
|
|
BEFORE UPDATE ON maintenance_schedules
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION update_updated_at_column();
|
|
```
|
|
|
|
## Category and Subtype Definitions
|
|
|
|
### Routine Maintenance (27 subtypes)
|
|
```
|
|
Accelerator Pedal
|
|
Air Filter Element
|
|
Brakes and Traction Control
|
|
Cabin Air Filter / Purifier
|
|
Coolant
|
|
Doors
|
|
Drive Belt
|
|
Engine Oil
|
|
Evaporative Emissions System
|
|
Exhaust System
|
|
Fluid - A/T
|
|
Fluid - Differential
|
|
Fluid - M/T
|
|
Fluid Filter - A/T
|
|
Fluids
|
|
Fuel Delivery and Air Induction
|
|
Hood Shock / Support
|
|
Neutral Safety Switch
|
|
Parking Brake System
|
|
Restraints and Safety Systems
|
|
Shift Interlock, A/T
|
|
Spark Plug
|
|
Steering and Suspension
|
|
Tires
|
|
Trunk / Liftgate Shock / Support
|
|
Washer Fluid
|
|
Wiper Blade
|
|
```
|
|
|
|
### Repair (5 subtypes)
|
|
```
|
|
Engine
|
|
Transmission
|
|
Drivetrain
|
|
Exterior
|
|
Interior
|
|
```
|
|
|
|
### Performance Upgrade (5 subtypes)
|
|
```
|
|
Engine
|
|
Drivetrain
|
|
Suspension
|
|
Wheels/Tires
|
|
Exterior
|
|
```
|
|
|
|
## Backend Implementation
|
|
|
|
### File Structure
|
|
```
|
|
backend/src/features/maintenance/
|
|
├── README.md # Feature documentation
|
|
├── index.ts # Public API exports
|
|
├── api/
|
|
│ ├── maintenance.routes.ts # Fastify routes
|
|
│ ├── maintenance.controller.ts # HTTP handlers
|
|
│ └── maintenance.validation.ts # Request validation
|
|
├── domain/
|
|
│ ├── maintenance.types.ts # TypeScript types + constants
|
|
│ └── maintenance.service.ts # Business logic
|
|
├── data/
|
|
│ └── maintenance.repository.ts # Database queries
|
|
├── migrations/
|
|
│ └── 002_recreate_maintenance_tables.sql
|
|
└── tests/
|
|
├── unit/
|
|
│ └── maintenance.service.test.ts
|
|
└── integration/
|
|
└── maintenance.integration.test.ts
|
|
```
|
|
|
|
### Phase 1: Domain Layer
|
|
|
|
**File: `backend/src/features/maintenance/domain/maintenance.types.ts`**
|
|
|
|
```typescript
|
|
/**
|
|
* @ai-summary Type definitions for maintenance feature
|
|
* @ai-context Supports three categories with specific subtypes, multiple selections allowed
|
|
*/
|
|
|
|
// Category types
|
|
export type MaintenanceCategory = 'routine_maintenance' | 'repair' | 'performance_upgrade';
|
|
|
|
// Subtype definitions (constants for validation)
|
|
export const ROUTINE_MAINTENANCE_SUBTYPES = [
|
|
'Accelerator Pedal',
|
|
'Air Filter Element',
|
|
'Brakes and Traction Control',
|
|
'Cabin Air Filter / Purifier',
|
|
'Coolant',
|
|
'Doors',
|
|
'Drive Belt',
|
|
'Engine Oil',
|
|
'Evaporative Emissions System',
|
|
'Exhaust System',
|
|
'Fluid - A/T',
|
|
'Fluid - Differential',
|
|
'Fluid - M/T',
|
|
'Fluid Filter - A/T',
|
|
'Fluids',
|
|
'Fuel Delivery and Air Induction',
|
|
'Hood Shock / Support',
|
|
'Neutral Safety Switch',
|
|
'Parking Brake System',
|
|
'Restraints and Safety Systems',
|
|
'Shift Interlock, A/T',
|
|
'Spark Plug',
|
|
'Steering and Suspension',
|
|
'Tires',
|
|
'Trunk / Liftgate Shock / Support',
|
|
'Washer Fluid',
|
|
'Wiper Blade'
|
|
] as const;
|
|
|
|
export const REPAIR_SUBTYPES = [
|
|
'Engine',
|
|
'Transmission',
|
|
'Drivetrain',
|
|
'Exterior',
|
|
'Interior'
|
|
] as const;
|
|
|
|
export const PERFORMANCE_UPGRADE_SUBTYPES = [
|
|
'Engine',
|
|
'Drivetrain',
|
|
'Suspension',
|
|
'Wheels/Tires',
|
|
'Exterior'
|
|
] as const;
|
|
|
|
// Database record types
|
|
export interface MaintenanceRecord {
|
|
id: string;
|
|
user_id: string;
|
|
vehicle_id: string;
|
|
category: MaintenanceCategory;
|
|
subtypes: string[];
|
|
date: string;
|
|
odometer_reading?: number;
|
|
cost?: number;
|
|
shop_name?: string;
|
|
notes?: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface MaintenanceSchedule {
|
|
id: string;
|
|
user_id: string;
|
|
vehicle_id: string;
|
|
category: MaintenanceCategory;
|
|
subtypes: string[];
|
|
interval_months?: number;
|
|
interval_miles?: number;
|
|
last_service_date?: string;
|
|
last_service_mileage?: number;
|
|
next_due_date?: string;
|
|
next_due_mileage?: number;
|
|
is_active: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
// Request types
|
|
export interface CreateMaintenanceRecordRequest {
|
|
vehicle_id: string;
|
|
category: MaintenanceCategory;
|
|
subtypes: string[]; // Must have at least one
|
|
date: string;
|
|
odometer_reading?: number;
|
|
cost?: number;
|
|
shop_name?: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface UpdateMaintenanceRecordRequest {
|
|
category?: MaintenanceCategory;
|
|
subtypes?: string[];
|
|
date?: string;
|
|
odometer_reading?: number;
|
|
cost?: number;
|
|
shop_name?: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface CreateScheduleRequest {
|
|
vehicle_id: string;
|
|
category: MaintenanceCategory;
|
|
subtypes: string[];
|
|
interval_months?: number;
|
|
interval_miles?: number;
|
|
}
|
|
|
|
export interface UpdateScheduleRequest {
|
|
category?: MaintenanceCategory;
|
|
subtypes?: string[];
|
|
interval_months?: number;
|
|
interval_miles?: number;
|
|
is_active?: boolean;
|
|
}
|
|
|
|
// Response types
|
|
export interface MaintenanceRecordResponse extends MaintenanceRecord {
|
|
subtype_count: number; // For list display: "Routine Maintenance (3)"
|
|
}
|
|
|
|
export interface MaintenanceScheduleResponse extends MaintenanceSchedule {
|
|
subtype_count: number;
|
|
is_due_soon?: boolean; // Within 30 days or 500 miles
|
|
is_overdue?: boolean;
|
|
}
|
|
|
|
// Validation helpers
|
|
export function getSubtypesForCategory(category: MaintenanceCategory): readonly string[] {
|
|
switch (category) {
|
|
case 'routine_maintenance': return ROUTINE_MAINTENANCE_SUBTYPES;
|
|
case 'repair': return REPAIR_SUBTYPES;
|
|
case 'performance_upgrade': return PERFORMANCE_UPGRADE_SUBTYPES;
|
|
}
|
|
}
|
|
|
|
export function validateSubtypes(category: MaintenanceCategory, subtypes: string[]): boolean {
|
|
if (!subtypes || subtypes.length === 0) return false;
|
|
const validSubtypes = getSubtypesForCategory(category);
|
|
return subtypes.every(st => validSubtypes.includes(st as any));
|
|
}
|
|
|
|
export function getCategoryDisplayName(category: MaintenanceCategory): string {
|
|
switch (category) {
|
|
case 'routine_maintenance': return 'Routine Maintenance';
|
|
case 'repair': return 'Repair';
|
|
case 'performance_upgrade': return 'Performance Upgrade';
|
|
}
|
|
}
|
|
```
|
|
|
|
**File: `backend/src/features/maintenance/domain/maintenance.service.ts`**
|
|
|
|
Key methods to implement:
|
|
- `createRecord(data, userId)` - Validate vehicle ownership, validate subtypes match category, create record
|
|
- `getRecords(userId, filters?)` - Get user's records, apply filters (vehicleId, category, dateRange)
|
|
- `getRecord(id, userId)` - Get single record with ownership check
|
|
- `updateRecord(id, data, userId)` - Update with validation
|
|
- `deleteRecord(id, userId)` - Soft delete or hard delete
|
|
- `getRecordsByVehicle(vehicleId, userId)` - Vehicle-specific records
|
|
|
|
- `createSchedule(data, userId)` - Create recurring schedule, calculate initial next_due
|
|
- `getSchedules(userId, filters?)` - Get schedules with filters
|
|
- `updateSchedule(id, data, userId)` - Update schedule, recalculate next_due if intervals change
|
|
- `deleteSchedule(id, userId)` - Remove schedule
|
|
- `getUpcomingMaintenance(vehicleId, userId)` - Get schedules that are due soon or overdue
|
|
|
|
- `calculateNextDue(schedule, currentDate, currentMileage)` - Calculate next due date/mileage based on intervals
|
|
|
|
**Cache Strategy:**
|
|
- Records: `maintenance:records:user:{userId}` - 5 min TTL
|
|
- Vehicle records: `maintenance:records:vehicle:{vehicleId}` - 5 min TTL
|
|
- Schedules: `maintenance:schedules:vehicle:{vehicleId}` - 5 min TTL
|
|
- Upcoming: `maintenance:upcoming:{vehicleId}` - 1 hour TTL
|
|
|
|
### Phase 2: Data Layer
|
|
|
|
**File: `backend/src/features/maintenance/data/maintenance.repository.ts`**
|
|
|
|
Key methods (all use prepared statements, all filter by user_id):
|
|
- `insert(record)` - INSERT with PostgreSQL array for subtypes
|
|
- `findById(id, userId)` - SELECT with user_id check
|
|
- `findByUserId(userId)` - SELECT user's records
|
|
- `findByVehicleId(vehicleId, userId)` - SELECT vehicle records with ownership check
|
|
- `update(id, userId, data)` - UPDATE with user_id check
|
|
- `delete(id, userId)` - DELETE with user_id check
|
|
|
|
- `insertSchedule(schedule)` - INSERT schedule
|
|
- `findSchedulesByVehicle(vehicleId, userId)` - SELECT vehicle schedules
|
|
- `updateSchedule(id, userId, data)` - UPDATE schedule
|
|
- `deleteSchedule(id, userId)` - DELETE schedule
|
|
- `findDueSchedules(vehicleId, userId, currentDate, currentMileage)` - Complex query for due/overdue
|
|
|
|
**PostgreSQL Array Handling:**
|
|
```typescript
|
|
// Insert with array
|
|
await pool.query(
|
|
'INSERT INTO maintenance_records (subtypes, ...) VALUES ($1, ...)',
|
|
[[subtype1, subtype2, subtype3], ...]
|
|
);
|
|
|
|
// Query with array contains
|
|
await pool.query(
|
|
'SELECT * FROM maintenance_records WHERE $1 = ANY(subtypes)',
|
|
[searchSubtype]
|
|
);
|
|
```
|
|
|
|
### Phase 3: API Layer
|
|
|
|
**File: `backend/src/features/maintenance/api/maintenance.routes.ts`**
|
|
|
|
```typescript
|
|
import { FastifyInstance } from 'fastify';
|
|
import { MaintenanceController } from './maintenance.controller';
|
|
|
|
export async function maintenanceRoutes(app: FastifyInstance) {
|
|
const controller = new MaintenanceController();
|
|
|
|
// All routes require authentication
|
|
app.addHook('preHandler', app.authenticate);
|
|
|
|
// Maintenance Records
|
|
app.post('/maintenance/records', controller.createRecord.bind(controller));
|
|
app.get('/maintenance/records', controller.listRecords.bind(controller));
|
|
app.get('/maintenance/records/:id', controller.getRecord.bind(controller));
|
|
app.put('/maintenance/records/:id', controller.updateRecord.bind(controller));
|
|
app.delete('/maintenance/records/:id', controller.deleteRecord.bind(controller));
|
|
app.get('/maintenance/records/vehicle/:vehicleId', controller.getRecordsByVehicle.bind(controller));
|
|
|
|
// Maintenance Schedules
|
|
app.post('/maintenance/schedules', controller.createSchedule.bind(controller));
|
|
app.get('/maintenance/schedules', controller.listSchedules.bind(controller));
|
|
app.get('/maintenance/schedules/vehicle/:vehicleId', controller.getSchedulesByVehicle.bind(controller));
|
|
app.put('/maintenance/schedules/:id', controller.updateSchedule.bind(controller));
|
|
app.delete('/maintenance/schedules/:id', controller.deleteSchedule.bind(controller));
|
|
|
|
// Utility endpoints
|
|
app.get('/maintenance/upcoming/:vehicleId', controller.getUpcomingMaintenance.bind(controller));
|
|
app.get('/maintenance/subtypes/:category', controller.getSubtypes.bind(controller));
|
|
}
|
|
```
|
|
|
|
**File: `backend/src/features/maintenance/api/maintenance.controller.ts`**
|
|
|
|
Follow pattern from `backend/src/features/documents/api/documents.controller.ts`:
|
|
- Extract userId from `request.user.sub`
|
|
- Use structured logging with logger
|
|
- Return proper HTTP status codes (201 for create, 200 for success, 404 for not found, etc.)
|
|
- Handle errors gracefully
|
|
|
|
**File: `backend/src/features/maintenance/api/maintenance.validation.ts`**
|
|
|
|
Use validation schemas (Fastify schema or Zod):
|
|
- Validate category is valid enum
|
|
- Validate subtypes is non-empty array
|
|
- Validate subtypes match category (server-side validation)
|
|
- Validate dates, numeric values
|
|
- Validate UUIDs
|
|
|
|
**File: `backend/src/features/maintenance/index.ts`**
|
|
|
|
```typescript
|
|
export { maintenanceRoutes } from './api/maintenance.routes';
|
|
export * from './domain/maintenance.types';
|
|
```
|
|
|
|
**File: `backend/src/app.ts`**
|
|
|
|
Update to register routes (remove lines 118-134 placeholder):
|
|
|
|
```typescript
|
|
import { maintenanceRoutes } from './features/maintenance';
|
|
|
|
// ... in buildApp()
|
|
await app.register(maintenanceRoutes, { prefix: '/api' });
|
|
```
|
|
|
|
### Phase 4: Testing
|
|
|
|
**File: `backend/src/features/maintenance/tests/unit/maintenance.service.test.ts`**
|
|
|
|
Test cases:
|
|
- Create record with valid data
|
|
- Reject invalid category
|
|
- Reject invalid subtypes for category
|
|
- Reject empty subtypes array
|
|
- Calculate next due date correctly
|
|
- Identify due soon vs overdue
|
|
- Handle edge cases (no previous service, etc.)
|
|
|
|
**File: `backend/src/features/maintenance/tests/integration/maintenance.integration.test.ts`**
|
|
|
|
Test full API workflow:
|
|
- Create, read, update, delete records
|
|
- Create and manage schedules
|
|
- Get upcoming maintenance
|
|
- Test authentication (reject without token)
|
|
- Test authorization (reject access to other user's data)
|
|
- Test PostgreSQL array operations
|
|
|
|
## Frontend Implementation
|
|
|
|
### File Structure
|
|
```
|
|
frontend/src/features/maintenance/
|
|
├── types/
|
|
│ └── maintenance.types.ts # Mirror backend types
|
|
├── api/
|
|
│ └── maintenance.api.ts # API client
|
|
├── hooks/
|
|
│ ├── useMaintenanceRecords.ts # Records query/mutation
|
|
│ ├── useMaintenanceSchedules.ts # Schedules query/mutation
|
|
│ └── useUpcomingMaintenance.ts # Upcoming items
|
|
├── components/ # Desktop components
|
|
│ ├── MaintenanceRecordForm.tsx
|
|
│ ├── MaintenanceRecordsList.tsx
|
|
│ ├── MaintenanceRecordDetail.tsx
|
|
│ ├── MaintenanceScheduleForm.tsx
|
|
│ ├── MaintenanceSchedulesList.tsx
|
|
│ ├── UpcomingMaintenanceCard.tsx
|
|
│ └── SubtypeCheckboxGroup.tsx # Reusable checkbox component
|
|
├── mobile/ # Mobile components
|
|
│ └── MaintenanceMobileScreen.tsx
|
|
└── pages/
|
|
└── MaintenancePage.tsx # Desktop page
|
|
```
|
|
|
|
### Phase 5: Frontend Types and API
|
|
|
|
**File: `frontend/src/features/maintenance/types/maintenance.types.ts`**
|
|
|
|
Copy types from backend, export constants for dropdowns.
|
|
|
|
**File: `frontend/src/features/maintenance/api/maintenance.api.ts`**
|
|
|
|
```typescript
|
|
import { api } from '../../../core/api/client';
|
|
|
|
export const maintenanceApi = {
|
|
// Records
|
|
createRecord: (data: CreateMaintenanceRecordRequest) =>
|
|
api.post('/api/maintenance/records', data),
|
|
|
|
getRecords: () =>
|
|
api.get<MaintenanceRecordResponse[]>('/api/maintenance/records'),
|
|
|
|
getRecord: (id: string) =>
|
|
api.get<MaintenanceRecordResponse>(`/api/maintenance/records/${id}`),
|
|
|
|
updateRecord: (id: string, data: UpdateMaintenanceRecordRequest) =>
|
|
api.put(`/api/maintenance/records/${id}`, data),
|
|
|
|
deleteRecord: (id: string) =>
|
|
api.delete(`/api/maintenance/records/${id}`),
|
|
|
|
getRecordsByVehicle: (vehicleId: string) =>
|
|
api.get<MaintenanceRecordResponse[]>(`/api/maintenance/records/vehicle/${vehicleId}`),
|
|
|
|
// Schedules
|
|
createSchedule: (data: CreateScheduleRequest) =>
|
|
api.post('/api/maintenance/schedules', data),
|
|
|
|
getSchedules: () =>
|
|
api.get<MaintenanceScheduleResponse[]>('/api/maintenance/schedules'),
|
|
|
|
getSchedulesByVehicle: (vehicleId: string) =>
|
|
api.get<MaintenanceScheduleResponse[]>(`/api/maintenance/schedules/vehicle/${vehicleId}`),
|
|
|
|
updateSchedule: (id: string, data: UpdateScheduleRequest) =>
|
|
api.put(`/api/maintenance/schedules/${id}`, data),
|
|
|
|
deleteSchedule: (id: string) =>
|
|
api.delete(`/api/maintenance/schedules/${id}`),
|
|
|
|
// Utility
|
|
getUpcoming: (vehicleId: string) =>
|
|
api.get<MaintenanceScheduleResponse[]>(`/api/maintenance/upcoming/${vehicleId}`),
|
|
|
|
getSubtypes: (category: MaintenanceCategory) =>
|
|
api.get<string[]>(`/api/maintenance/subtypes/${category}`)
|
|
};
|
|
```
|
|
|
|
### Phase 6: React Hooks
|
|
|
|
**File: `frontend/src/features/maintenance/hooks/useMaintenanceRecords.ts`**
|
|
|
|
Use React Query pattern from fuel-logs:
|
|
|
|
```typescript
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { maintenanceApi } from '../api/maintenance.api';
|
|
|
|
export function useMaintenanceRecords(vehicleId?: string) {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: records, isLoading, error } = useQuery({
|
|
queryKey: vehicleId ? ['maintenance-records', vehicleId] : ['maintenance-records'],
|
|
queryFn: () => vehicleId
|
|
? maintenanceApi.getRecordsByVehicle(vehicleId)
|
|
: maintenanceApi.getRecords()
|
|
});
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: maintenanceApi.createRecord,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['maintenance-records'] });
|
|
}
|
|
});
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: UpdateMaintenanceRecordRequest }) =>
|
|
maintenanceApi.updateRecord(id, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['maintenance-records'] });
|
|
}
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: maintenanceApi.deleteRecord,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['maintenance-records'] });
|
|
}
|
|
});
|
|
|
|
return {
|
|
records,
|
|
isLoading,
|
|
error,
|
|
createRecord: createMutation.mutate,
|
|
updateRecord: updateMutation.mutate,
|
|
deleteRecord: deleteMutation.mutate
|
|
};
|
|
}
|
|
```
|
|
|
|
Similar hooks for schedules and upcoming maintenance.
|
|
|
|
### Phase 7: Desktop Components
|
|
|
|
**File: `frontend/src/features/maintenance/components/SubtypeCheckboxGroup.tsx`**
|
|
|
|
Reusable component for subtype selection:
|
|
|
|
```typescript
|
|
interface SubtypeCheckboxGroupProps {
|
|
category: MaintenanceCategory;
|
|
selectedSubtypes: string[];
|
|
onChange: (subtypes: string[]) => void;
|
|
}
|
|
|
|
export function SubtypeCheckboxGroup({ category, selectedSubtypes, onChange }: SubtypeCheckboxGroupProps) {
|
|
const subtypes = getSubtypesForCategory(category);
|
|
|
|
const handleToggle = (subtype: string) => {
|
|
if (selectedSubtypes.includes(subtype)) {
|
|
onChange(selectedSubtypes.filter(s => s !== subtype));
|
|
} else {
|
|
onChange([...selectedSubtypes, subtype]);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
|
|
{subtypes.map(subtype => (
|
|
<FormControlLabel
|
|
key={subtype}
|
|
control={
|
|
<Checkbox
|
|
checked={selectedSubtypes.includes(subtype)}
|
|
onChange={() => handleToggle(subtype)}
|
|
/>
|
|
}
|
|
label={subtype}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**File: `frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx`**
|
|
|
|
Form structure:
|
|
1. Category dropdown (Routine Maintenance, Repair, Performance Upgrade)
|
|
2. SubtypeCheckboxGroup (dynamically shows based on category)
|
|
3. Date picker
|
|
4. Odometer input (optional)
|
|
5. Cost input (optional)
|
|
6. Shop name input (optional)
|
|
7. Notes textarea (optional)
|
|
8. Submit and Cancel buttons
|
|
|
|
Validation:
|
|
- Category required
|
|
- At least one subtype required
|
|
- Selected subtypes must match category
|
|
- Date required
|
|
- Show error messages inline
|
|
|
|
**File: `frontend/src/features/maintenance/components/MaintenanceRecordsList.tsx`**
|
|
|
|
Table or card list showing:
|
|
- Date (sortable, default newest first)
|
|
- Category with count: "Routine Maintenance (3)"
|
|
- Odometer reading
|
|
- Cost
|
|
- Shop name
|
|
- Actions (view details, edit, delete)
|
|
|
|
Click row to navigate to detail view.
|
|
|
|
**File: `frontend/src/features/maintenance/components/MaintenanceRecordDetail.tsx`**
|
|
|
|
Full detail view:
|
|
- All fields displayed
|
|
- Subtypes shown as chips/badges
|
|
- Edit and Delete buttons
|
|
- Back button
|
|
|
|
**File: `frontend/src/features/maintenance/pages/MaintenancePage.tsx`**
|
|
|
|
Tabbed interface:
|
|
- **Records Tab**: List of completed maintenance
|
|
- **Schedules Tab**: Recurring maintenance schedules
|
|
- **Upcoming Tab**: Due soon and overdue items
|
|
|
|
Include:
|
|
- Vehicle selector (dropdown)
|
|
- Add new record/schedule buttons
|
|
- Filters (category, date range)
|
|
|
|
### Phase 8: Mobile Components
|
|
|
|
**File: `frontend/src/features/maintenance/mobile/MaintenanceMobileScreen.tsx`**
|
|
|
|
Mobile-optimized design:
|
|
- GlassCard components (match existing pattern from documents/fuel-logs)
|
|
- Touch-friendly form inputs
|
|
- Large checkboxes (min 44x44px touch target)
|
|
- Single column layout
|
|
- Bottom sheet or full-screen modal for add/edit forms
|
|
- Swipe actions for delete
|
|
- Pull to refresh
|
|
|
|
Collapsible sections:
|
|
- Tap category to expand/collapse subtype checkboxes
|
|
- Accordion style to save vertical space
|
|
|
|
**Mobile-specific considerations:**
|
|
- Virtual scrolling for long lists
|
|
- Optimistic updates for instant feedback
|
|
- Loading skeletons
|
|
- Error boundaries
|
|
|
|
### Phase 9: Route Integration
|
|
|
|
**File: `frontend/src/App.tsx`**
|
|
|
|
Desktop routes (update line 554):
|
|
```typescript
|
|
import { lazy } from 'react';
|
|
const MaintenancePage = lazy(() => import('./features/maintenance/pages/MaintenancePage').then(m => ({ default: m.MaintenancePage })));
|
|
|
|
// In Routes:
|
|
<Route path="/maintenance" element={<MaintenancePage />} />
|
|
```
|
|
|
|
Mobile navigation:
|
|
- Maintenance already in Layout.tsx navigation (line 42)
|
|
- Consider if it should be in bottom nav or remain in hamburger menu
|
|
- If adding to bottom nav, update mobile nav items in App.tsx
|
|
|
|
## Display Format Guidelines
|
|
|
|
### List View (Records)
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Jan 15, 2024 │
|
|
│ Routine Maintenance (3) │
|
|
│ 45,230 miles | $127.50 | Joe's Auto │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### Detail View (Full Record)
|
|
```
|
|
Date: January 15, 2024
|
|
Category: Routine Maintenance
|
|
Subtypes:
|
|
• Engine Oil
|
|
• Air Filter Element
|
|
• Cabin Air Filter / Purifier
|
|
Odometer: 45,230 miles
|
|
Cost: $127.50
|
|
Shop: Joe's Auto Service
|
|
Notes: Used synthetic 5W-30 oil. Recommended tire rotation at next visit.
|
|
```
|
|
|
|
### Upcoming Maintenance (Color-Coded)
|
|
```
|
|
🟢 Good - Not due yet
|
|
🟡 Due Soon - Within 30 days or 500 miles
|
|
🔴 Overdue - Past due date or mileage
|
|
```
|
|
|
|
## Business Rules
|
|
|
|
### Validation Rules
|
|
1. Category must be one of: routine_maintenance, repair, performance_upgrade
|
|
2. Subtypes must be non-empty array
|
|
3. All subtypes must be valid for the selected category
|
|
4. Date required for records
|
|
5. Vehicle must belong to user (ownership check)
|
|
6. Interval (months OR miles OR both) required for schedules
|
|
|
|
### Next Due Calculation (Schedules)
|
|
```
|
|
If interval_months AND interval_miles both set:
|
|
next_due_date = last_service_date + interval_months
|
|
next_due_mileage = last_service_mileage + interval_miles
|
|
Due when EITHER condition is met (whichever comes first)
|
|
|
|
If only interval_months:
|
|
next_due_date = last_service_date + interval_months
|
|
next_due_mileage = null
|
|
|
|
If only interval_miles:
|
|
next_due_date = null
|
|
next_due_mileage = last_service_mileage + interval_miles
|
|
```
|
|
|
|
### Due Soon Logic
|
|
```
|
|
Due Soon (Yellow):
|
|
- next_due_date within 30 days of today
|
|
- OR next_due_mileage within 500 miles of current odometer
|
|
|
|
Overdue (Red):
|
|
- next_due_date in the past
|
|
- OR next_due_mileage < current odometer
|
|
```
|
|
|
|
## Security Requirements
|
|
|
|
1. **All queries user-scoped**: Every database query MUST filter by user_id
|
|
2. **Vehicle ownership**: Validate user owns vehicle before any operation
|
|
3. **Prepared statements**: NEVER concatenate SQL strings
|
|
4. **Authentication**: All routes require valid JWT token
|
|
5. **Authorization**: Users can only access their own data
|
|
|
|
Example repository pattern:
|
|
```typescript
|
|
async findByVehicleId(vehicleId: string, userId: string): Promise<MaintenanceRecord[]> {
|
|
// CORRECT - filters by both vehicle_id AND user_id
|
|
const result = await pool.query(
|
|
'SELECT * FROM maintenance_records WHERE vehicle_id = $1 AND user_id = $2',
|
|
[vehicleId, userId]
|
|
);
|
|
return result.rows;
|
|
}
|
|
```
|
|
|
|
## Caching Strategy
|
|
|
|
Use Redis for caching:
|
|
|
|
```typescript
|
|
// Records cache - 5 minutes
|
|
const cacheKey = `maintenance:records:user:${userId}`;
|
|
const ttl = 300; // 5 minutes
|
|
|
|
// Vehicle-specific cache - 5 minutes
|
|
const vehicleCacheKey = `maintenance:records:vehicle:${vehicleId}`;
|
|
|
|
// Upcoming maintenance - 1 hour (less frequently changing)
|
|
const upcomingCacheKey = `maintenance:upcoming:${vehicleId}`;
|
|
const upcomingTTL = 3600; // 1 hour
|
|
|
|
// Invalidate on create/update/delete
|
|
await cacheService.del(cacheKey);
|
|
await cacheService.del(vehicleCacheKey);
|
|
await cacheService.del(upcomingCacheKey);
|
|
```
|
|
|
|
## Testing Strategy
|
|
|
|
### Backend Tests
|
|
|
|
**Unit Tests** (`backend/src/features/maintenance/tests/unit/`)
|
|
- Test service methods with mocked repository
|
|
- Test validation logic (category, subtypes)
|
|
- Test next due calculation
|
|
- Test due soon/overdue logic
|
|
- Test edge cases (no previous service, missing data)
|
|
|
|
**Integration Tests** (`backend/src/features/maintenance/tests/integration/`)
|
|
- Test full API endpoints with test database
|
|
- Test authentication (401 without token)
|
|
- Test authorization (403 for other user's data)
|
|
- Test PostgreSQL array operations
|
|
- Test cascade deletes (vehicle deletion)
|
|
|
|
Run tests:
|
|
```bash
|
|
make shell-backend
|
|
npm test -- features/maintenance
|
|
```
|
|
|
|
### Frontend Tests
|
|
|
|
Test components:
|
|
- Form validation (category, subtypes)
|
|
- Checkbox selection/deselection
|
|
- Mobile touch interactions
|
|
- Responsive layout
|
|
|
|
### Manual Testing (Docker-Only)
|
|
|
|
1. After each change: `make rebuild`
|
|
2. Test mobile viewport: 375px width
|
|
3. Test desktop viewport: 1920px width
|
|
4. Test touch interactions on mobile
|
|
5. Verify all linting hooks pass (zero tolerance)
|
|
|
|
## Documentation Requirements
|
|
|
|
**File: `backend/src/features/maintenance/README.md`**
|
|
|
|
Follow pattern from fuel-logs README:
|
|
|
|
```markdown
|
|
# Maintenance Feature Capsule
|
|
|
|
## Quick Summary (50 tokens)
|
|
Tracks vehicle maintenance including routine service, repairs, and performance upgrades. Supports multiple subtypes per record, recurring schedules, and upcoming/overdue calculations. User-scoped data with vehicle ownership enforcement.
|
|
|
|
## API Endpoints
|
|
[List all endpoints with descriptions]
|
|
|
|
## Structure
|
|
- **api/** - HTTP endpoints, routes, validators
|
|
- **domain/** - Business logic, types, rules
|
|
- **data/** - Repository, database queries
|
|
- **migrations/** - Feature-specific schema
|
|
- **tests/** - All feature tests
|
|
|
|
## Categories and Subtypes
|
|
[List all three categories and their subtypes]
|
|
|
|
## Dependencies
|
|
- Internal: core/auth, core/cache, core/config
|
|
- Database: maintenance_records, maintenance_schedules tables
|
|
- Feature: vehicles (vehicle_id FK)
|
|
|
|
## Business Rules
|
|
[Document validation, calculation logic, etc.]
|
|
|
|
## Testing
|
|
[Document test commands and examples]
|
|
```
|
|
|
|
## Success Criteria Checklist
|
|
|
|
- [ ] Database migrations run cleanly (`make migrate`)
|
|
- [ ] Backend compiles without errors
|
|
- [ ] All backend unit tests pass
|
|
- [ ] All backend integration tests pass
|
|
- [ ] All TypeScript types are correct (no `any` types)
|
|
- [ ] All linting rules pass (ESLint, Prettier)
|
|
- [ ] Category dropdown works correctly
|
|
- [ ] Subtype checkboxes populate based on selected category
|
|
- [ ] Multiple subtypes can be selected
|
|
- [ ] Subtype validation prevents invalid selections
|
|
- [ ] Records display as "Category (count)" in list view
|
|
- [ ] Detail view shows all selected subtypes
|
|
- [ ] Schedule creation works
|
|
- [ ] Next due date calculation is correct
|
|
- [ ] Upcoming maintenance shows correct items
|
|
- [ ] Works on mobile (375px viewport)
|
|
- [ ] Touch targets are 44x44px minimum on mobile
|
|
- [ ] Works on desktop (1920px viewport)
|
|
- [ ] Responsive between mobile and desktop breakpoints
|
|
- [ ] No console errors
|
|
- [ ] No TODOs remaining in code
|
|
- [ ] README.md is complete and accurate
|
|
- [ ] Feature is registered in backend app.ts
|
|
- [ ] Feature is registered in frontend App.tsx routes
|
|
|
|
## Common Pitfalls to Avoid
|
|
|
|
1. **PostgreSQL Arrays**: Use proper array syntax `TEXT[]` and array operations `= ANY(subtypes)`
|
|
2. **User Scoping**: NEVER forget `AND user_id = $X` in queries
|
|
3. **Category Validation**: Server-side validation is required (don't trust client)
|
|
4. **Empty Subtypes**: Validate array is non-empty before saving
|
|
5. **Mobile Touch Targets**: Checkboxes must be 44x44px minimum
|
|
6. **Cache Invalidation**: Invalidate ALL relevant cache keys on update
|
|
7. **String Concatenation**: NEVER concatenate SQL strings - use prepared statements
|
|
8. **Type Safety**: Don't use `any` types - define proper interfaces
|
|
|
|
## Reference Files
|
|
|
|
For implementation patterns, refer to these existing features:
|
|
- **Documents Feature**: `backend/src/features/documents/` (most recent, best example)
|
|
- **Fuel Logs Feature**: `backend/src/features/fuel-logs/` (similar complexity)
|
|
- **Documents Frontend**: `frontend/src/features/documents/` (mobile + desktop patterns)
|
|
|
|
## Implementation Order
|
|
|
|
1. Backend database migration
|
|
2. Backend domain types
|
|
3. Backend repository
|
|
4. Backend service
|
|
5. Backend API (routes, controller, validation)
|
|
6. Backend tests
|
|
7. Register routes in app.ts
|
|
8. Frontend types
|
|
9. Frontend API client
|
|
10. Frontend hooks
|
|
11. Desktop components (form, list, detail)
|
|
12. Desktop page with tabs
|
|
13. Mobile components
|
|
14. Update routes in App.tsx
|
|
15. Manual testing (docker rebuild)
|
|
16. Documentation (README.md)
|
|
17. Final validation (all criteria met)
|