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
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:
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
</Box>
|
{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>
|
||||||
|
|
||||||
|
{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' }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user