Files
motovaultpro/docs/changes/database-20251111/frontend-api-client.md
2025-11-11 10:29:02 -06:00

598 lines
18 KiB
Markdown

# Frontend API Client Update - Agent 5
## Task: Update vehicles API client to use string-based parameters and responses
**Status**: Ready for Implementation
**Dependencies**: Agent 4 (Vehicles API) must be complete
**Estimated Time**: 1 hour
**Assigned To**: Agent 5 (Frontend API)
---
## Overview
Update the frontend API client to match the new backend API contract. Change from ID-based parameters to string-based parameters, and handle string array responses instead of object arrays.
---
## Prerequisites
### Verify Agent 4 Completed
```bash
# Test backend API returns strings
curl -X GET "http://localhost:3000/api/vehicles/dropdown/makes?year=2024" \
-H "Authorization: Bearer TOKEN"
# Should return: ["Ford", "Honda", ...] not [{id: 1, name: "Ford"}, ...]
```
### Files to Modify
```
frontend/src/features/vehicles/api/vehicles.api.ts
frontend/src/features/vehicles/types/vehicles.types.ts
```
---
## Current Implementation Analysis
### API Client
**File**: `frontend/src/features/vehicles/api/vehicles.api.ts`
**Current Methods** (ID-based parameters, object responses):
```typescript
getMakes(year: number): Promise<DropdownOption[]>
getModels(year: number, makeId: number): Promise<DropdownOption[]>
getTrims(year: number, makeId: number, modelId: number): Promise<DropdownOption[]>
getEngines(year: number, makeId: number, modelId: number, trimId: number): Promise<DropdownOption[]>
getTransmissions(year: number, makeId: number, modelId: number): Promise<DropdownOption[]>
getYears(): Promise<number[]>
```
### Type Definition
**File**: `frontend/src/features/vehicles/types/vehicles.types.ts`
**Current DropdownOption**:
```typescript
export interface DropdownOption {
id: number;
name: string;
}
```
---
## Step 1: Update or Remove DropdownOption Type
**File**: `frontend/src/features/vehicles/types/vehicles.types.ts`
**Option A: Remove DropdownOption entirely** (Recommended)
**Find and delete** (lines 56-59):
```typescript
export interface DropdownOption {
id: number;
name: string;
}
```
**Reason**: API now returns plain `string[]`, no need for wrapper type
---
**Option B: Keep as alias** (If other code depends on it)
**Replace with**:
```typescript
// Deprecated: Dropdowns now return string[] directly
// Keeping for backward compatibility during migration
export type DropdownOption = string;
```
**Recommendation**: Use Option A (delete entirely). Clean break, clear intent.
---
## Step 2: Update API Client Imports
**File**: `frontend/src/features/vehicles/api/vehicles.api.ts`
**Current** (line 6):
```typescript
import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, DropdownOption, VINDecodeResponse } from '../types/vehicles.types';
```
**New**:
```typescript
import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, VINDecodeResponse } from '../types/vehicles.types';
// DropdownOption removed - using string[] now
```
---
## Step 3: Update getMakes()
**Current** (lines 41-44):
```typescript
getMakes: async (year: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
return response.data;
},
```
**New**:
```typescript
getMakes: async (year: number): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
return response.data;
},
```
**Changes**:
- Line 41: Return type `Promise<DropdownOption[]>``Promise<string[]>`
- Query parameter unchanged (already uses year)
- Response data is now string array
---
## Step 4: Update getModels()
**Current** (lines 46-49):
```typescript
getModels: async (year: number, makeId: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make_id=${makeId}`);
return response.data;
},
```
**New**:
```typescript
getModels: async (year: number, make: string): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make=${encodeURIComponent(make)}`);
return response.data;
},
```
**Changes**:
- Line 46: Parameter `makeId: number``make: string`
- Line 46: Return type `Promise<DropdownOption[]>``Promise<string[]>`
- Line 47: Query param `make_id=${makeId}``make=${encodeURIComponent(make)}`
- **Important**: Use `encodeURIComponent()` to handle spaces and special chars in make names
---
## Step 5: Update getTrims()
**Current** (lines 61-64):
```typescript
getTrims: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make_id=${makeId}&model_id=${modelId}`);
return response.data;
},
```
**New**:
```typescript
getTrims: async (year: number, make: string, model: string): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
return response.data;
},
```
**Changes**:
- Line 61: Parameters changed to `make: string, model: string`
- Line 61: Return type changed to `Promise<string[]>`
- Line 62: Query params changed to `make=...&model=...`
- Use `encodeURIComponent()` for both make and model
---
## Step 6: Update getEngines()
**Current** (lines 56-59):
```typescript
getEngines: async (year: number, makeId: number, modelId: number, trimId: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make_id=${makeId}&model_id=${modelId}&trim_id=${trimId}`);
return response.data;
},
```
**New**:
```typescript
getEngines: async (year: number, make: string, model: string, trim: string): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}&trim=${encodeURIComponent(trim)}`);
return response.data;
},
```
**Changes**:
- Line 56: Parameters changed to strings: `make: string, model: string, trim: string`
- Line 56: Return type changed to `Promise<string[]>`
- Line 57: Query params changed to use strings with proper encoding
- Use `encodeURIComponent()` for all string parameters
---
## Step 7: Update getTransmissions()
**Current** (lines 51-54):
```typescript
getTransmissions: async (year: number, makeId: number, modelId: number): Promise<DropdownOption[]> => {
const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make_id=${makeId}&model_id=${modelId}`);
return response.data;
},
```
**New**:
```typescript
getTransmissions: async (year: number, make: string, model: string): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
return response.data;
},
```
**Changes**:
- Line 51: Parameters changed to `make: string, model: string`
- Line 51: Return type changed to `Promise<string[]>`
- Line 52: Query params changed to strings
- Use `encodeURIComponent()` for encoding
---
## Complete Updated File
After all changes, the vehicles.api.ts file should look like:
```typescript
/**
* @ai-summary API calls for vehicles feature
*/
import { apiClient } from '../../../core/api/client';
import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, VINDecodeResponse } from '../types/vehicles.types';
// All requests (including dropdowns) use authenticated apiClient
export const vehiclesApi = {
getAll: async (): Promise<Vehicle[]> => {
const response = await apiClient.get('/vehicles');
return response.data;
},
getById: async (id: string): Promise<Vehicle> => {
const response = await apiClient.get(`/vehicles/${id}`);
return response.data;
},
create: async (data: CreateVehicleRequest): Promise<Vehicle> => {
const response = await apiClient.post('/vehicles', data);
return response.data;
},
update: async (id: string, data: UpdateVehicleRequest): Promise<Vehicle> => {
const response = await apiClient.put(`/vehicles/${id}`, data);
return response.data;
},
delete: async (id: string): Promise<void> => {
await apiClient.delete(`/vehicles/${id}`);
},
// Dropdown API methods - now return string[] and accept string parameters
getYears: async (): Promise<number[]> => {
const response = await apiClient.get('/vehicles/dropdown/years');
return response.data;
},
getMakes: async (year: number): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/makes?year=${year}`);
return response.data;
},
getModels: async (year: number, make: string): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/models?year=${year}&make=${encodeURIComponent(make)}`);
return response.data;
},
getTrims: async (year: number, make: string, model: string): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/trims?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
return response.data;
},
getEngines: async (year: number, make: string, model: string, trim: string): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/engines?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}&trim=${encodeURIComponent(trim)}`);
return response.data;
},
getTransmissions: async (year: number, make: string, model: string): Promise<string[]> => {
const response = await apiClient.get(`/vehicles/dropdown/transmissions?year=${year}&make=${encodeURIComponent(make)}&model=${encodeURIComponent(model)}`);
return response.data;
},
// VIN decode method - using unified platform endpoint
decodeVIN: async (vin: string): Promise<VINDecodeResponse> => {
const response = await apiClient.get(`/platform/vehicle?vin=${vin}`);
return response.data;
},
};
```
---
## Why encodeURIComponent()?
**Problem**: Vehicle makes/models can contain spaces and special characters:
- "Land Rover"
- "Mercedes-Benz"
- "Alfa Romeo"
**Without encoding**: Query string breaks
```
/vehicles/dropdown/models?year=2024&make=Land Rover
^ space breaks URL
```
**With encoding**: Query string is valid
```
/vehicles/dropdown/models?year=2024&make=Land%20Rover
```
**Rule**: Always encode user-provided strings in URLs
---
## Testing & Verification
### Manual Testing
Test the API client methods in browser console:
```typescript
// In browser console (after logging in)
import { vehiclesApi } from './src/features/vehicles/api/vehicles.api';
// Test getMakes - should return string[]
const makes = await vehiclesApi.getMakes(2024);
console.log(makes); // Expected: ["Acura", "Audi", "BMW", ...]
console.log(typeof makes[0]); // Expected: "string"
// Test getModels with string parameter
const models = await vehiclesApi.getModels(2024, 'Ford');
console.log(models); // Expected: ["Bronco", "Edge", "F-150", ...]
// Test with space in name
const landRoverModels = await vehiclesApi.getModels(2024, 'Land Rover');
console.log(landRoverModels); // Should work - no URL errors
// Test getTrims
const trims = await vehiclesApi.getTrims(2024, 'Ford', 'F-150');
console.log(trims); // Expected: ["XLT", "Lariat", ...]
// Test getEngines
const engines = await vehiclesApi.getEngines(2024, 'Ford', 'F-150', 'XLT');
console.log(engines); // Expected: ["V6 2.7L Turbo", "V8 5.0L", ...]
// Test getTransmissions - should return REAL data now
const transmissions = await vehiclesApi.getTransmissions(2024, 'Ford', 'F-150');
console.log(transmissions); // Expected: ["10-Speed Automatic", ...] NOT ["Automatic", "Manual"]
```
### Unit Tests
Create tests for the API client:
```typescript
// frontend/src/features/vehicles/api/__tests__/vehicles.api.test.ts
import { vehiclesApi } from '../vehicles.api';
import { apiClient } from '../../../../core/api/client';
jest.mock('../../../../core/api/client');
describe('vehiclesApi', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('getMakes', () => {
it('should return string array', async () => {
const mockMakes = ['Ford', 'Honda', 'Toyota'];
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockMakes });
const result = await vehiclesApi.getMakes(2024);
expect(result).toEqual(mockMakes);
expect(apiClient.get).toHaveBeenCalledWith('/vehicles/dropdown/makes?year=2024');
});
});
describe('getModels', () => {
it('should use make string parameter', async () => {
const mockModels = ['F-150', 'Mustang'];
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockModels });
const result = await vehiclesApi.getModels(2024, 'Ford');
expect(result).toEqual(mockModels);
expect(apiClient.get).toHaveBeenCalledWith('/vehicles/dropdown/models?year=2024&make=Ford');
});
it('should encode make parameter with spaces', async () => {
const mockModels = ['Range Rover', 'Defender'];
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockModels });
await vehiclesApi.getModels(2024, 'Land Rover');
expect(apiClient.get).toHaveBeenCalledWith('/vehicles/dropdown/models?year=2024&make=Land%20Rover');
});
it('should encode special characters', async () => {
const mockModels = ['C-Class'];
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockModels });
await vehiclesApi.getModels(2024, 'Mercedes-Benz');
expect(apiClient.get).toHaveBeenCalledWith(expect.stringContaining('Mercedes-Benz'));
});
});
describe('getTransmissions', () => {
it('should return real transmission data', async () => {
const mockTransmissions = ['10-Speed Automatic', '6-Speed Manual'];
(apiClient.get as jest.Mock).mockResolvedValue({ data: mockTransmissions });
const result = await vehiclesApi.getTransmissions(2024, 'Ford', 'F-150');
expect(result).toEqual(mockTransmissions);
// Verify not getting old hardcoded data
expect(result).not.toEqual([{id: 1, name: 'Automatic'}, {id: 2, name: 'Manual'}]);
});
});
});
```
Run tests:
```bash
cd frontend
npm test -- vehicles.api.test.ts
```
---
## Completion Checklist
Before signaling completion:
- [ ] DropdownOption type removed (or deprecated)
- [ ] Import statement updated (removed DropdownOption)
- [ ] getMakes() returns string[]
- [ ] getModels() accepts make string, returns string[]
- [ ] getTrims() accepts make/model strings, returns string[]
- [ ] getEngines() accepts make/model/trim strings, returns string[]
- [ ] getTransmissions() accepts make/model strings, returns string[]
- [ ] All string parameters use encodeURIComponent()
- [ ] Query parameters changed: make_id→make, model_id→model, trim_id→trim
- [ ] Manual tests pass (no type errors, correct responses)
- [ ] Unit tests pass
- [ ] TypeScript compiles with no errors
- [ ] No console errors in browser
---
## Common Issues
### Issue: TypeScript error "Type 'string[]' is not assignable to 'DropdownOption[]'"
**Cause**: Form component (Agent 6) not yet updated, still expects DropdownOption[]
**Solution**:
- This is expected - Agent 6 will update the form component
- Verify API client changes are correct
- TypeScript errors will resolve once Agent 6 completes
### Issue: URL encoding breaks query
**Cause**: Forgot `encodeURIComponent()` on string parameters
**Solution**:
```typescript
// Wrong
`/models?make=${make}` // Breaks with "Land Rover"
// Correct
`/models?make=${encodeURIComponent(make)}` // Handles all cases
```
### Issue: Backend returns 400 "make parameter required"
**Cause**: Frontend sending wrong parameter name or encoding issue
**Solution**:
- Check browser Network tab for actual request
- Verify parameter names: `make`, `model`, `trim` (not `make_id`, etc.)
- Verify values are properly encoded
---
## Handoff to Agent 6
Once complete, provide this information:
### Updated API Client Contract
**Methods**:
```typescript
getYears(): Promise<number[]>
getMakes(year: number): Promise<string[]>
getModels(year: number, make: string): Promise<string[]>
getTrims(year: number, make: string, model: string): Promise<string[]>
getEngines(year: number, make: string, model: string, trim: string): Promise<string[]>
getTransmissions(year: number, make: string, model: string): Promise<string[]>
decodeVIN(vin: string): Promise<VINDecodeResponse>
```
**Key Changes for Agent 6**:
- All dropdown methods return `string[]` (not `DropdownOption[]`)
- Cascade queries use selected string values directly (not IDs)
- No need to extract `.id` from selected options anymore
- No need to find selected option by ID to get name
- Simpler state management - store strings directly
**Example Usage**:
```typescript
// OLD (ID-based)
const [selectedMake, setSelectedMake] = useState<DropdownOption | null>(null);
const models = await vehiclesApi.getModels(year, selectedMake.id); // Use ID
// Store name: vehicle.make = selectedMake.name
// NEW (String-based)
const [selectedMake, setSelectedMake] = useState<string>('');
const models = await vehiclesApi.getModels(year, selectedMake); // Use string directly
// Store directly: vehicle.make = selectedMake
```
### Verification Command
```bash
# Agent 6 can verify API client is ready:
cd frontend && npm run build
# Should compile (may have type errors in form component - Agent 6 will fix)
```
---
## Completion Message Template
```
Agent 5 (Frontend API Client): COMPLETE
Files Modified:
- frontend/src/features/vehicles/api/vehicles.api.ts
- frontend/src/features/vehicles/types/vehicles.types.ts
Changes Made:
- Removed DropdownOption interface (using string[] now)
- Updated all dropdown methods to return string[]
- Changed parameters from IDs to strings (make, model, trim)
- Added encodeURIComponent() for URL encoding
- Query parameters updated: make_id→make, model_id→model, trim_id→trim
Verification:
✓ TypeScript compiles (with expected form component errors)
✓ Manual API tests return correct string arrays
✓ Unit tests pass
✓ URL encoding handles spaces and special characters
✓ Transmissions return real data (not hardcoded)
Agent 6 (Frontend Forms) can now update VehicleForm component to use new API contract.
Breaking Changes for Agent 6:
- API methods return string[] not DropdownOption[]
- No more .id property - use selected strings directly
- Simplified cascade logic - no ID lookups needed
- Form can store selected values directly as strings
```
---
**Document Version**: 1.0
**Last Updated**: 2025-11-10
**Status**: Ready for Implementation