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