598 lines
18 KiB
Markdown
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
|