From 59e7f4053acf0766cf35985da7f78fec5d4bed68 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:47:46 -0600 Subject: [PATCH] fix: Data validation for scheduled maintenance --- frontend/.claude/tdd-guard/data/test.json | 20 ++++++ .../MaintenanceScheduleReviewScreen.test.tsx | 66 +++++++++++++++++-- .../MaintenanceScheduleReviewScreen.tsx | 17 ++++- .../hooks/useCreateSchedulesFromExtraction.ts | 1 + 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/frontend/.claude/tdd-guard/data/test.json b/frontend/.claude/tdd-guard/data/test.json index a9ae213..7ec14ca 100644 --- a/frontend/.claude/tdd-guard/data/test.json +++ b/frontend/.claude/tdd-guard/data/test.json @@ -88,6 +88,26 @@ "fullName": "MaintenanceScheduleReviewScreen Subtype validation should hide warning alert after deselecting items with empty subtypes", "state": "passed" }, + { + "name": "should disable create button when selected item has no intervals", + "fullName": "MaintenanceScheduleReviewScreen Interval validation should disable create button when selected item has no intervals", + "state": "passed" + }, + { + "name": "should enable create button after deselecting item with missing intervals", + "fullName": "MaintenanceScheduleReviewScreen Interval validation should enable create button after deselecting item with missing intervals", + "state": "passed" + }, + { + "name": "should show warning alert for items missing intervals", + "fullName": "MaintenanceScheduleReviewScreen Interval validation should show warning alert for items missing intervals", + "state": "passed" + }, + { + "name": "should enable create button after editing interval on item", + "fullName": "MaintenanceScheduleReviewScreen Interval validation should enable create button after editing interval on item", + "state": "passed" + }, { "name": "should render in fullscreen mode on mobile viewports", "fullName": "MaintenanceScheduleReviewScreen Responsive layout should render in fullscreen mode on mobile viewports", diff --git a/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.test.tsx b/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.test.tsx index 534e007..7de7ace 100644 --- a/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.test.tsx +++ b/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.test.tsx @@ -75,7 +75,7 @@ const sampleItems: MaintenanceScheduleItem[] = [ }, ]; -const sampleItemsWithEmpty: MaintenanceScheduleItem[] = [ +const sampleItemsWithEmptySubtypes: MaintenanceScheduleItem[] = [ ...sampleItems, { service: 'Brake Fluid', @@ -87,6 +87,18 @@ const sampleItemsWithEmpty: MaintenanceScheduleItem[] = [ }, ]; +const sampleItemsWithMissingIntervals: MaintenanceScheduleItem[] = [ + ...sampleItems, + { + service: 'Coolant Flush', + intervalMiles: null, + intervalMonths: null, + details: null, + confidence: 0.55, + subtypes: ['Coolant'], + }, +]; + describe('MaintenanceScheduleReviewScreen', () => { const defaultProps = { open: true, @@ -306,7 +318,7 @@ describe('MaintenanceScheduleReviewScreen', () => { describe('Subtype validation', () => { it('should disable create button when selected item has empty subtypes', () => { - render(); + render(); // All 4 items selected, but Brake Fluid has no subtypes const createButton = screen.getByRole('button', { name: /create/i }); @@ -314,7 +326,7 @@ describe('MaintenanceScheduleReviewScreen', () => { }); it('should enable create button after deselecting item with empty subtypes', () => { - render(); + render(); // Deselect the 4th item (Brake Fluid with empty subtypes) const checkboxes = screen.getAllByRole('checkbox'); @@ -325,13 +337,13 @@ describe('MaintenanceScheduleReviewScreen', () => { }); it('should show warning alert for items missing subtypes', () => { - render(); + render(); expect(screen.getByText(/missing subtypes/)).toBeInTheDocument(); }); it('should hide warning alert after deselecting items with empty subtypes', () => { - render(); + render(); // Deselect the Brake Fluid item const checkboxes = screen.getAllByRole('checkbox'); @@ -341,6 +353,50 @@ describe('MaintenanceScheduleReviewScreen', () => { }); }); + describe('Interval validation', () => { + it('should disable create button when selected item has no intervals', () => { + render(); + + const createButton = screen.getByRole('button', { name: /create/i }); + expect(createButton).toBeDisabled(); + }); + + it('should enable create button after deselecting item with missing intervals', () => { + render(); + + // Deselect the 4th item (Coolant Flush with null intervals) + const checkboxes = screen.getAllByRole('checkbox'); + fireEvent.click(checkboxes[3]); + + const createButton = screen.getByRole('button', { name: /create 3 schedules/i }); + expect(createButton).not.toBeDisabled(); + }); + + it('should show warning alert for items missing intervals', () => { + render(); + + expect(screen.getByText(/missing intervals/)).toBeInTheDocument(); + }); + + it('should enable create button after editing interval on item', () => { + render(); + + // The Coolant Flush item shows '-' for both intervals. Click the Miles '-' to edit. + // There are multiple '-' on screen, so find all and pick the right one. + const dashTexts = screen.getAllByText('-'); + // Click the first dash (Miles field of the Coolant Flush item - last item's first dash) + fireEvent.click(dashTexts[dashTexts.length - 2]); + + // Type a value and save + const input = screen.getByDisplayValue(''); + fireEvent.change(input, { target: { value: '50000' } }); + fireEvent.keyDown(input, { key: 'Enter' }); + + const createButton = screen.getByRole('button', { name: /create 4 schedules/i }); + expect(createButton).not.toBeDisabled(); + }); + }); + describe('Responsive layout', () => { afterEach(() => { // Reset matchMedia after each test diff --git a/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.tsx b/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.tsx index 289a572..4e243bd 100644 --- a/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.tsx +++ b/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.tsx @@ -191,6 +191,9 @@ export const MaintenanceScheduleReviewScreen: React.FC i.selected).length; const hasInvalidSubtypes = editableItems.some((i) => i.selected && i.subtypes.length === 0); + const hasInvalidIntervals = editableItems.some( + (i) => i.selected && i.intervalMiles === null && i.intervalMonths === null + ); const handleToggle = useCallback((index: number) => { setEditableItems((prev) => @@ -326,6 +329,7 @@ export const MaintenanceScheduleReviewScreen: React.FC handleFieldUpdate(index, 'intervalMonths', v)} suffix="mo" /> + {item.selected && item.intervalMiles === null && item.intervalMonths === null && ( + + + + )} {item.details && ( @@ -377,6 +386,12 @@ export const MaintenanceScheduleReviewScreen: React.FC )} + {hasInvalidIntervals && ( + + Some selected items are missing intervals. Please set at least one interval (miles or months) for each selected item. + + )} + Tap any field to edit before creating schedules. @@ -409,7 +424,7 @@ export const MaintenanceScheduleReviewScreen: React.FC : } sx={{ minHeight: 44, order: isMobile ? 1 : 2, width: isMobile ? '100%' : 'auto' }} > diff --git a/frontend/src/features/maintenance/hooks/useCreateSchedulesFromExtraction.ts b/frontend/src/features/maintenance/hooks/useCreateSchedulesFromExtraction.ts index 934b057..cbd9d93 100644 --- a/frontend/src/features/maintenance/hooks/useCreateSchedulesFromExtraction.ts +++ b/frontend/src/features/maintenance/hooks/useCreateSchedulesFromExtraction.ts @@ -21,6 +21,7 @@ export function useCreateSchedulesFromExtraction() { const results: MaintenanceScheduleResponse[] = []; for (const item of items) { if (item.subtypes.length === 0) continue; + if (item.intervalMiles === null && item.intervalMonths === null) continue; const request: CreateScheduleRequest = { vehicleId, category: 'routine_maintenance',