fix: Data validation for scheduled maintenance
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 3m24s
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:47:46 -06:00
parent 33b489d526
commit 59e7f4053a
4 changed files with 98 additions and 6 deletions

View File

@@ -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",

View File

@@ -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(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmpty} />);
render(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmptySubtypes} />);
// 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(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmpty} />);
render(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmptySubtypes} />);
// 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(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmpty} />);
render(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmptySubtypes} />);
expect(screen.getByText(/missing subtypes/)).toBeInTheDocument();
});
it('should hide warning alert after deselecting items with empty subtypes', () => {
render(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmpty} />);
render(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithEmptySubtypes} />);
// 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(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithMissingIntervals} />);
const createButton = screen.getByRole('button', { name: /create/i });
expect(createButton).toBeDisabled();
});
it('should enable create button after deselecting item with missing intervals', () => {
render(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithMissingIntervals} />);
// 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(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithMissingIntervals} />);
expect(screen.getByText(/missing intervals/)).toBeInTheDocument();
});
it('should enable create button after editing interval on item', () => {
render(<MaintenanceScheduleReviewScreen {...defaultProps} items={sampleItemsWithMissingIntervals} />);
// 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

View File

@@ -191,6 +191,9 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
const selectedCount = editableItems.filter((i) => 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<MaintenanceScheduleReview
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
gap: isMobile ? 0.5 : 2,
alignItems: 'center',
}}>
<InlineField
label="Miles"
@@ -341,6 +345,11 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
onSave={(v) => handleFieldUpdate(index, 'intervalMonths', v)}
suffix="mo"
/>
{item.selected && item.intervalMiles === null && item.intervalMonths === null && (
<Tooltip title="At least one interval (miles or months) is required">
<WarningAmberIcon color="warning" sx={{ fontSize: 18 }} />
</Tooltip>
)}
</Box>
{item.details && (
@@ -377,6 +386,12 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
</Alert>
)}
{hasInvalidIntervals && (
<Alert severity="warning" sx={{ mt: 2 }}>
Some selected items are missing intervals. Please set at least one interval (miles or months) for each selected item.
</Alert>
)}
<Typography variant="body2" color="text.secondary" sx={{ mt: 2, textAlign: 'center' }}>
Tap any field to edit before creating schedules.
</Typography>
@@ -409,7 +424,7 @@ export const MaintenanceScheduleReviewScreen: React.FC<MaintenanceScheduleReview
<Button
variant="contained"
onClick={handleCreate}
disabled={selectedCount === 0 || hasInvalidSubtypes || createMutation.isPending}
disabled={selectedCount === 0 || hasInvalidSubtypes || hasInvalidIntervals || createMutation.isPending}
startIcon={createMutation.isPending ? <CircularProgress size={16} /> : <CheckIcon />}
sx={{ minHeight: 44, order: isMobile ? 1 : 2, width: isMobile ? '100%' : 'auto' }}
>

View File

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