Files
motovaultpro/docs/MAINTENANCE-FEATURE-PLAN.md
Eric Gullickson 5638d3960b Update
2025-10-16 19:20:30 -05:00

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)