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

32 KiB

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

-- 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

/**
 * @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:

// 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

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

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):

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

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:

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:

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):

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:

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:

// 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:

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:

# 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)