diff --git a/frontend/.claude/tdd-guard/data/test.json b/frontend/.claude/tdd-guard/data/test.json
index bf2a3fc..a9ae213 100644
--- a/frontend/.claude/tdd-guard/data/test.json
+++ b/frontend/.claude/tdd-guard/data/test.json
@@ -19,8 +19,8 @@
"state": "passed"
},
{
- "name": "should display subtype chips",
- "fullName": "MaintenanceScheduleReviewScreen Rendering should display subtype chips",
+ "name": "should display subtypes in SubtypeCheckboxGroup",
+ "fullName": "MaintenanceScheduleReviewScreen Rendering should display subtypes in SubtypeCheckboxGroup",
"state": "passed"
},
{
@@ -68,6 +68,26 @@
"fullName": "MaintenanceScheduleReviewScreen Editing should update item data via inline editing",
"state": "passed"
},
+ {
+ "name": "should disable create button when selected item has empty subtypes",
+ "fullName": "MaintenanceScheduleReviewScreen Subtype validation should disable create button when selected item has empty subtypes",
+ "state": "passed"
+ },
+ {
+ "name": "should enable create button after deselecting item with empty subtypes",
+ "fullName": "MaintenanceScheduleReviewScreen Subtype validation should enable create button after deselecting item with empty subtypes",
+ "state": "passed"
+ },
+ {
+ "name": "should show warning alert for items missing subtypes",
+ "fullName": "MaintenanceScheduleReviewScreen Subtype validation should show warning alert for items missing subtypes",
+ "state": "passed"
+ },
+ {
+ "name": "should hide warning alert after deselecting items with empty subtypes",
+ "fullName": "MaintenanceScheduleReviewScreen Subtype validation should hide warning alert after deselecting items with empty subtypes",
+ "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 16e9ea8..534e007 100644
--- a/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.test.tsx
+++ b/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.test.tsx
@@ -33,6 +33,21 @@ jest.mock('../hooks/useCreateSchedulesFromExtraction', () => ({
}),
}));
+// Track SubtypeCheckboxGroup onChange callbacks per instance
+const subtypeOnChangeCallbacks: Array<(subtypes: string[]) => void> = [];
+jest.mock('./SubtypeCheckboxGroup', () => ({
+ SubtypeCheckboxGroup: ({ selected, onChange }: { category: string; selected: string[]; onChange: (subtypes: string[]) => void }) => {
+ subtypeOnChangeCallbacks.push(onChange);
+ return (
+
+ {selected.map((s: string) => (
+ {s}
+ ))}
+
+ );
+ },
+}));
+
const sampleItems: MaintenanceScheduleItem[] = [
{
service: 'Engine Oil Change',
@@ -60,6 +75,18 @@ const sampleItems: MaintenanceScheduleItem[] = [
},
];
+const sampleItemsWithEmpty: MaintenanceScheduleItem[] = [
+ ...sampleItems,
+ {
+ service: 'Brake Fluid',
+ intervalMiles: 30000,
+ intervalMonths: 24,
+ details: null,
+ confidence: 0.65,
+ subtypes: [],
+ },
+];
+
describe('MaintenanceScheduleReviewScreen', () => {
const defaultProps = {
open: true,
@@ -72,6 +99,7 @@ describe('MaintenanceScheduleReviewScreen', () => {
beforeEach(() => {
jest.clearAllMocks();
mockMutateAsync.mockResolvedValue([]);
+ subtypeOnChangeCallbacks.length = 0;
});
describe('Rendering', () => {
@@ -109,9 +137,12 @@ describe('MaintenanceScheduleReviewScreen', () => {
expect(screen.getByText('Use 0W-20 full synthetic oil')).toBeInTheDocument();
});
- it('should display subtype chips', () => {
+ it('should display subtypes in SubtypeCheckboxGroup', () => {
render();
+ const groups = screen.getAllByTestId('subtype-checkbox-group');
+ expect(groups).toHaveLength(3);
+
expect(screen.getByText('Engine Oil')).toBeInTheDocument();
expect(screen.getByText('Tires')).toBeInTheDocument();
expect(screen.getByText('Cabin Air Filter / Purifier')).toBeInTheDocument();
@@ -273,6 +304,43 @@ describe('MaintenanceScheduleReviewScreen', () => {
});
});
+ describe('Subtype validation', () => {
+ it('should disable create button when selected item has empty subtypes', () => {
+ render();
+
+ // All 4 items selected, but Brake Fluid has no subtypes
+ const createButton = screen.getByRole('button', { name: /create/i });
+ expect(createButton).toBeDisabled();
+ });
+
+ it('should enable create button after deselecting item with empty subtypes', () => {
+ render();
+
+ // Deselect the 4th item (Brake Fluid with empty subtypes)
+ 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 subtypes', () => {
+ render();
+
+ expect(screen.getByText(/missing subtypes/)).toBeInTheDocument();
+ });
+
+ it('should hide warning alert after deselecting items with empty subtypes', () => {
+ render();
+
+ // Deselect the Brake Fluid item
+ const checkboxes = screen.getAllByRole('checkbox');
+ fireEvent.click(checkboxes[3]);
+
+ expect(screen.queryByText(/missing subtypes/)).not.toBeInTheDocument();
+ });
+ });
+
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 259dc60..289a572 100644
--- a/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.tsx
+++ b/frontend/src/features/maintenance/components/MaintenanceScheduleReviewScreen.tsx
@@ -17,7 +17,7 @@ import {
IconButton,
Alert,
CircularProgress,
- Chip,
+ Tooltip,
useTheme,
useMediaQuery,
} from '@mui/material';
@@ -26,8 +26,11 @@ import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close';
import SelectAllIcon from '@mui/icons-material/SelectAll';
import DeselectIcon from '@mui/icons-material/Deselect';
+import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import type { MaintenanceScheduleItem } from '../../documents/hooks/useManualExtraction';
import { useCreateSchedulesFromExtraction } from '../hooks/useCreateSchedulesFromExtraction';
+import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup';
+import { getSubtypesForCategory } from '../types/maintenance.types';
export interface MaintenanceScheduleReviewScreenProps {
open: boolean;
@@ -176,12 +179,18 @@ export const MaintenanceScheduleReviewScreen: React.FC(() =>
- items.map((item) => ({ ...item, selected: true }))
+ items.map((item) => ({
+ ...item,
+ subtypes: item.subtypes.filter((st) => validRoutineSubtypes.includes(st)),
+ selected: true,
+ }))
);
const [createError, setCreateError] = useState(null);
const selectedCount = editableItems.filter((i) => i.selected).length;
+ const hasInvalidSubtypes = editableItems.some((i) => i.selected && i.subtypes.length === 0);
const handleToggle = useCallback((index: number) => {
setEditableItems((prev) =>
@@ -203,6 +212,12 @@ export const MaintenanceScheduleReviewScreen: React.FC {
+ setEditableItems((prev) =>
+ prev.map((item, i) => (i === index ? { ...item, subtypes } : item))
+ );
+ }, []);
+
const handleCreate = async () => {
setCreateError(null);
const selectedItems = editableItems.filter((i) => i.selected);
@@ -334,18 +349,34 @@ export const MaintenanceScheduleReviewScreen: React.FC
)}
- {item.subtypes.length > 0 && (
-
- {item.subtypes.map((subtype) => (
-
- ))}
+
+
+
+ Subtypes:
+
+ {item.subtypes.length === 0 && item.selected && (
+
+
+
+ )}
- )}
+ handleSubtypesChange(index, subtypes)}
+ />
+
))}
+ {hasInvalidSubtypes && (
+
+ Some selected items are missing subtypes. Please select at least one subtype for each selected item.
+
+ )}
+
Tap any field to edit before creating schedules.
@@ -378,7 +409,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 cb3da87..934b057 100644
--- a/frontend/src/features/maintenance/hooks/useCreateSchedulesFromExtraction.ts
+++ b/frontend/src/features/maintenance/hooks/useCreateSchedulesFromExtraction.ts
@@ -20,10 +20,11 @@ export function useCreateSchedulesFromExtraction() {
mutationFn: async ({ vehicleId, items }) => {
const results: MaintenanceScheduleResponse[] = [];
for (const item of items) {
+ if (item.subtypes.length === 0) continue;
const request: CreateScheduleRequest = {
vehicleId,
category: 'routine_maintenance',
- subtypes: item.subtypes.length > 0 ? item.subtypes : [],
+ subtypes: item.subtypes,
scheduleType: 'interval',
intervalMiles: item.intervalMiles ?? undefined,
intervalMonths: item.intervalMonths ?? undefined,