fix: Update auto schedule creation
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m29s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 25s
Deploy to Staging / Verify Staging (pull_request) Successful in 8s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 7s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped

This commit is contained in:
Eric Gullickson
2026-02-11 20:29:33 -06:00
parent 55a7bcc874
commit 33b489d526
4 changed files with 133 additions and 13 deletions

View File

@@ -19,8 +19,8 @@
"state": "passed" "state": "passed"
}, },
{ {
"name": "should display subtype chips", "name": "should display subtypes in SubtypeCheckboxGroup",
"fullName": "MaintenanceScheduleReviewScreen Rendering should display subtype chips", "fullName": "MaintenanceScheduleReviewScreen Rendering should display subtypes in SubtypeCheckboxGroup",
"state": "passed" "state": "passed"
}, },
{ {
@@ -68,6 +68,26 @@
"fullName": "MaintenanceScheduleReviewScreen Editing should update item data via inline editing", "fullName": "MaintenanceScheduleReviewScreen Editing should update item data via inline editing",
"state": "passed" "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", "name": "should render in fullscreen mode on mobile viewports",
"fullName": "MaintenanceScheduleReviewScreen Responsive layout should render in fullscreen mode on mobile viewports", "fullName": "MaintenanceScheduleReviewScreen Responsive layout should render in fullscreen mode on mobile viewports",

View File

@@ -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 (
<div data-testid="subtype-checkbox-group">
{selected.map((s: string) => (
<span key={s} data-testid="subtype-chip">{s}</span>
))}
</div>
);
},
}));
const sampleItems: MaintenanceScheduleItem[] = [ const sampleItems: MaintenanceScheduleItem[] = [
{ {
service: 'Engine Oil Change', 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', () => { describe('MaintenanceScheduleReviewScreen', () => {
const defaultProps = { const defaultProps = {
open: true, open: true,
@@ -72,6 +99,7 @@ describe('MaintenanceScheduleReviewScreen', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
mockMutateAsync.mockResolvedValue([]); mockMutateAsync.mockResolvedValue([]);
subtypeOnChangeCallbacks.length = 0;
}); });
describe('Rendering', () => { describe('Rendering', () => {
@@ -109,9 +137,12 @@ describe('MaintenanceScheduleReviewScreen', () => {
expect(screen.getByText('Use 0W-20 full synthetic oil')).toBeInTheDocument(); expect(screen.getByText('Use 0W-20 full synthetic oil')).toBeInTheDocument();
}); });
it('should display subtype chips', () => { it('should display subtypes in SubtypeCheckboxGroup', () => {
render(<MaintenanceScheduleReviewScreen {...defaultProps} />); render(<MaintenanceScheduleReviewScreen {...defaultProps} />);
const groups = screen.getAllByTestId('subtype-checkbox-group');
expect(groups).toHaveLength(3);
expect(screen.getByText('Engine Oil')).toBeInTheDocument(); expect(screen.getByText('Engine Oil')).toBeInTheDocument();
expect(screen.getByText('Tires')).toBeInTheDocument(); expect(screen.getByText('Tires')).toBeInTheDocument();
expect(screen.getByText('Cabin Air Filter / Purifier')).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(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmpty} />);
// 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(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmpty} />);
// 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(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmpty} />);
expect(screen.getByText(/missing subtypes/)).toBeInTheDocument();
});
it('should hide warning alert after deselecting items with empty subtypes', () => {
render(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmpty} />);
// Deselect the Brake Fluid item
const checkboxes = screen.getAllByRole('checkbox');
fireEvent.click(checkboxes[3]);
expect(screen.queryByText(/missing subtypes/)).not.toBeInTheDocument();
});
});
describe('Responsive layout', () => { describe('Responsive layout', () => {
afterEach(() => { afterEach(() => {
// Reset matchMedia after each test // Reset matchMedia after each test

View File

@@ -17,7 +17,7 @@ import {
IconButton, IconButton,
Alert, Alert,
CircularProgress, CircularProgress,
Chip, Tooltip,
useTheme, useTheme,
useMediaQuery, useMediaQuery,
} from '@mui/material'; } from '@mui/material';
@@ -26,8 +26,11 @@ import CheckIcon from '@mui/icons-material/Check';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import SelectAllIcon from '@mui/icons-material/SelectAll'; import SelectAllIcon from '@mui/icons-material/SelectAll';
import DeselectIcon from '@mui/icons-material/Deselect'; import DeselectIcon from '@mui/icons-material/Deselect';
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
import type { MaintenanceScheduleItem } from '../../documents/hooks/useManualExtraction'; import type { MaintenanceScheduleItem } from '../../documents/hooks/useManualExtraction';
import { useCreateSchedulesFromExtraction } from '../hooks/useCreateSchedulesFromExtraction'; import { useCreateSchedulesFromExtraction } from '../hooks/useCreateSchedulesFromExtraction';
import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup';
import { getSubtypesForCategory } from '../types/maintenance.types';
export interface MaintenanceScheduleReviewScreenProps { export interface MaintenanceScheduleReviewScreenProps {
open: boolean; open: boolean;
@@ -176,12 +179,18 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const createMutation = useCreateSchedulesFromExtraction(); const createMutation = useCreateSchedulesFromExtraction();
const validRoutineSubtypes = getSubtypesForCategory('routine_maintenance');
const [editableItems, setEditableItems] = useState<EditableItem[]>(() => const [editableItems, setEditableItems] = useState<EditableItem[]>(() =>
items.map((item) => ({ ...item, selected: true })) items.map((item) => ({
...item,
subtypes: item.subtypes.filter((st) => validRoutineSubtypes.includes(st)),
selected: true,
}))
); );
const [createError, setCreateError] = useState<string | null>(null); const [createError, setCreateError] = useState<string | null>(null);
const selectedCount = editableItems.filter((i) => i.selected).length; const selectedCount = editableItems.filter((i) => i.selected).length;
const hasInvalidSubtypes = editableItems.some((i) => i.selected && i.subtypes.length === 0);
const handleToggle = useCallback((index: number) => { const handleToggle = useCallback((index: number) => {
setEditableItems((prev) => setEditableItems((prev) =>
@@ -203,6 +212,12 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
); );
}, []); }, []);
const handleSubtypesChange = useCallback((index: number, subtypes: string[]) => {
setEditableItems((prev) =>
prev.map((item, i) => (i === index ? { ...item, subtypes } : item))
);
}, []);
const handleCreate = async () => { const handleCreate = async () => {
setCreateError(null); setCreateError(null);
const selectedItems = editableItems.filter((i) => i.selected); const selectedItems = editableItems.filter((i) => i.selected);
@@ -334,18 +349,34 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
</Typography> </Typography>
)} )}
{item.subtypes.length > 0 && ( <Box sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
{item.subtypes.map((subtype) => ( <Typography variant="caption" color="text.secondary">
<Chip key={subtype} label={subtype} size="small" variant="outlined" /> Subtypes:
))} </Typography>
{item.subtypes.length === 0 && item.selected && (
<Tooltip title="At least one subtype is required">
<WarningAmberIcon color="warning" sx={{ fontSize: 18 }} />
</Tooltip>
)}
</Box> </Box>
)} <SubtypeCheckboxGroup
category="routine_maintenance"
selected={item.subtypes}
onChange={(subtypes) => handleSubtypesChange(index, subtypes)}
/>
</Box>
</Box> </Box>
</Box> </Box>
))} ))}
</Box> </Box>
{hasInvalidSubtypes && (
<Alert severity="warning" sx={{ mt: 2 }}>
Some selected items are missing subtypes. Please select at least one subtype for each selected item.
</Alert>
)}
<Typography variant="body2" color="text.secondary" sx={{ mt: 2, textAlign: 'center' }}> <Typography variant="body2" color="text.secondary" sx={{ mt: 2, textAlign: 'center' }}>
Tap any field to edit before creating schedules. Tap any field to edit before creating schedules.
</Typography> </Typography>
@@ -378,7 +409,7 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
<Button <Button
variant="contained" variant="contained"
onClick={handleCreate} onClick={handleCreate}
disabled={selectedCount === 0 || createMutation.isPending} disabled={selectedCount === 0 || hasInvalidSubtypes || createMutation.isPending}
startIcon={createMutation.isPending ? <CircularProgress size={16} /> : <CheckIcon />} startIcon={createMutation.isPending ? <CircularProgress size={16} /> : <CheckIcon />}
sx={{ minHeight: 44, order: isMobile ? 1 : 2, width: isMobile ? '100%' : 'auto' }} sx={{ minHeight: 44, order: isMobile ? 1 : 2, width: isMobile ? '100%' : 'auto' }}
> >

View File

@@ -20,10 +20,11 @@ export function useCreateSchedulesFromExtraction() {
mutationFn: async ({ vehicleId, items }) => { mutationFn: async ({ vehicleId, items }) => {
const results: MaintenanceScheduleResponse[] = []; const results: MaintenanceScheduleResponse[] = [];
for (const item of items) { for (const item of items) {
if (item.subtypes.length === 0) continue;
const request: CreateScheduleRequest = { const request: CreateScheduleRequest = {
vehicleId, vehicleId,
category: 'routine_maintenance', category: 'routine_maintenance',
subtypes: item.subtypes.length > 0 ? item.subtypes : [], subtypes: item.subtypes,
scheduleType: 'interval', scheduleType: 'interval',
intervalMiles: item.intervalMiles ?? undefined, intervalMiles: item.intervalMiles ?? undefined,
intervalMonths: item.intervalMonths ?? undefined, intervalMonths: item.intervalMonths ?? undefined,