All checks were successful
Deploy to Staging / Build Images (push) Successful in 1m21s
Deploy to Staging / Deploy to Staging (push) Successful in 43s
Deploy to Staging / Verify Staging (push) Successful in 4s
Deploy to Staging / Notify Staging Ready (push) Successful in 4s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
482 lines
16 KiB
TypeScript
482 lines
16 KiB
TypeScript
/**
|
|
* @ai-summary Dialog for editing maintenance schedules
|
|
* @ai-context Modal form following MaintenanceRecordEditDialog pattern
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Button,
|
|
TextField,
|
|
Box,
|
|
Grid,
|
|
FormControl,
|
|
InputLabel,
|
|
Select,
|
|
MenuItem,
|
|
Typography,
|
|
useMediaQuery,
|
|
FormControlLabel,
|
|
Switch,
|
|
RadioGroup,
|
|
Radio,
|
|
FormLabel,
|
|
} from '@mui/material';
|
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
import dayjs from 'dayjs';
|
|
import {
|
|
MaintenanceScheduleResponse,
|
|
UpdateScheduleRequest,
|
|
MaintenanceCategory,
|
|
ScheduleType,
|
|
getCategoryDisplayName,
|
|
} from '../types/maintenance.types';
|
|
import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup';
|
|
import { useVehicles } from '../../vehicles/hooks/useVehicles';
|
|
import type { Vehicle } from '../../vehicles/types/vehicles.types';
|
|
import { getVehicleLabel } from '@/core/utils/vehicleDisplay';
|
|
|
|
interface MaintenanceScheduleEditDialogProps {
|
|
open: boolean;
|
|
schedule: MaintenanceScheduleResponse | null;
|
|
onClose: () => void;
|
|
onSave: (id: string, data: UpdateScheduleRequest) => Promise<void>;
|
|
}
|
|
|
|
export const MaintenanceScheduleEditDialog: React.FC<MaintenanceScheduleEditDialogProps> = ({
|
|
open,
|
|
schedule,
|
|
onClose,
|
|
onSave,
|
|
}) => {
|
|
const [formData, setFormData] = useState<UpdateScheduleRequest>({});
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
|
|
const vehiclesQuery = useVehicles();
|
|
const vehicles = vehiclesQuery.data;
|
|
const isSmallScreen = useMediaQuery('(max-width:600px)');
|
|
|
|
// Reset form when schedule changes
|
|
useEffect(() => {
|
|
if (schedule && schedule.id) {
|
|
try {
|
|
setFormData({
|
|
category: schedule.category,
|
|
subtypes: schedule.subtypes,
|
|
scheduleType: schedule.scheduleType,
|
|
intervalMonths: schedule.intervalMonths || undefined,
|
|
intervalMiles: schedule.intervalMiles || undefined,
|
|
fixedDueDate: schedule.fixedDueDate || undefined,
|
|
isActive: schedule.isActive,
|
|
emailNotifications: schedule.emailNotifications || false,
|
|
reminderDays1: schedule.reminderDays1 || undefined,
|
|
reminderDays2: schedule.reminderDays2 || undefined,
|
|
reminderDays3: schedule.reminderDays3 || undefined,
|
|
});
|
|
setError(null);
|
|
} catch (err) {
|
|
console.error('[MaintenanceScheduleEditDialog] Error setting form data:', err);
|
|
setError(err as Error);
|
|
}
|
|
}
|
|
}, [schedule]);
|
|
|
|
const handleInputChange = (field: keyof UpdateScheduleRequest, value: any) => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
};
|
|
|
|
const handleScheduleTypeChange = (newType: ScheduleType) => {
|
|
setFormData((prev) => {
|
|
const updated: UpdateScheduleRequest = {
|
|
...prev,
|
|
scheduleType: newType,
|
|
};
|
|
|
|
// Clear fields that don't apply to new schedule type
|
|
if (newType === 'interval') {
|
|
updated.fixedDueDate = null;
|
|
} else if (newType === 'fixed_date') {
|
|
updated.intervalMonths = null;
|
|
updated.intervalMiles = null;
|
|
} else if (newType === 'time_since_last') {
|
|
updated.fixedDueDate = null;
|
|
}
|
|
|
|
return updated;
|
|
});
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!schedule || !schedule.id) {
|
|
console.error('[MaintenanceScheduleEditDialog] No valid schedule to save');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsSaving(true);
|
|
|
|
// Filter out unchanged fields
|
|
const changedData: UpdateScheduleRequest = {};
|
|
Object.entries(formData).forEach(([key, value]) => {
|
|
const typedKey = key as keyof UpdateScheduleRequest;
|
|
const scheduleValue = schedule[typedKey as keyof MaintenanceScheduleResponse];
|
|
|
|
// Special handling for arrays
|
|
if (Array.isArray(value) && Array.isArray(scheduleValue)) {
|
|
if (JSON.stringify(value) !== JSON.stringify(scheduleValue)) {
|
|
(changedData as any)[key] = value;
|
|
}
|
|
} else if (value !== scheduleValue) {
|
|
(changedData as any)[key] = value;
|
|
}
|
|
});
|
|
|
|
// Only send update if there are actual changes
|
|
if (Object.keys(changedData).length > 0) {
|
|
await onSave(schedule.id, changedData);
|
|
}
|
|
|
|
onClose();
|
|
} catch (err) {
|
|
console.error('[MaintenanceScheduleEditDialog] Failed to save schedule:', err);
|
|
setError(err as Error);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
onClose();
|
|
};
|
|
|
|
// Early bailout if dialog not open or no schedule to edit
|
|
if (!open || !schedule) return null;
|
|
|
|
// Error state
|
|
if (error) {
|
|
return (
|
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
|
<DialogTitle>Error Loading Maintenance Schedule</DialogTitle>
|
|
<DialogContent>
|
|
<Typography color="error">
|
|
Failed to load maintenance schedule data. Please try again.
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
|
{error.message}
|
|
</Typography>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={onClose}>Close</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
const currentScheduleType = formData.scheduleType || 'interval';
|
|
|
|
return (
|
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
|
<Dialog
|
|
open={open}
|
|
onClose={handleCancel}
|
|
maxWidth="md"
|
|
fullWidth
|
|
fullScreen={isSmallScreen}
|
|
PaperProps={{
|
|
sx: { maxHeight: '90vh' },
|
|
}}
|
|
>
|
|
<DialogTitle>Edit Maintenance Schedule</DialogTitle>
|
|
<DialogContent>
|
|
<Box sx={{ mt: 1 }}>
|
|
<Grid container spacing={2}>
|
|
{/* Vehicle (Read-only display) */}
|
|
<Grid item xs={12}>
|
|
<TextField
|
|
label="Vehicle"
|
|
fullWidth
|
|
disabled
|
|
value={(() => {
|
|
const vehicle = vehicles?.find((v: Vehicle) => v.id === schedule.vehicleId);
|
|
return getVehicleLabel(vehicle);
|
|
})()}
|
|
helperText="Vehicle cannot be changed when editing"
|
|
/>
|
|
</Grid>
|
|
|
|
{/* Active Status */}
|
|
<Grid item xs={12}>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={formData.isActive ?? true}
|
|
onChange={(e) => handleInputChange('isActive', e.target.checked)}
|
|
/>
|
|
}
|
|
label="Schedule is active"
|
|
/>
|
|
<Typography variant="caption" color="text.secondary" display="block" sx={{ ml: 4 }}>
|
|
Inactive schedules will not trigger reminders
|
|
</Typography>
|
|
</Grid>
|
|
|
|
{/* Category */}
|
|
<Grid item xs={12}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Category</InputLabel>
|
|
<Select
|
|
value={formData.category || ''}
|
|
onChange={(e) =>
|
|
handleInputChange('category', e.target.value as MaintenanceCategory)
|
|
}
|
|
label="Category"
|
|
>
|
|
<MenuItem value="routine_maintenance">
|
|
{getCategoryDisplayName('routine_maintenance')}
|
|
</MenuItem>
|
|
<MenuItem value="repair">{getCategoryDisplayName('repair')}</MenuItem>
|
|
<MenuItem value="performance_upgrade">
|
|
{getCategoryDisplayName('performance_upgrade')}
|
|
</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
{/* Subtypes */}
|
|
{formData.category && (
|
|
<Grid item xs={12}>
|
|
<Typography variant="subtitle2" gutterBottom>
|
|
Service Types *
|
|
</Typography>
|
|
<SubtypeCheckboxGroup
|
|
category={formData.category}
|
|
selected={formData.subtypes || []}
|
|
onChange={(subtypes) => handleInputChange('subtypes', subtypes)}
|
|
/>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Schedule Type */}
|
|
<Grid item xs={12}>
|
|
<FormControl component="fieldset">
|
|
<FormLabel component="legend">Schedule Type</FormLabel>
|
|
<RadioGroup
|
|
value={currentScheduleType}
|
|
onChange={(e) => handleScheduleTypeChange(e.target.value as ScheduleType)}
|
|
>
|
|
<FormControlLabel
|
|
value="interval"
|
|
control={<Radio />}
|
|
label="Interval-based (months or miles)"
|
|
/>
|
|
<FormControlLabel
|
|
value="fixed_date"
|
|
control={<Radio />}
|
|
label="Fixed due date"
|
|
/>
|
|
<FormControlLabel
|
|
value="time_since_last"
|
|
control={<Radio />}
|
|
label="Time since last service"
|
|
/>
|
|
</RadioGroup>
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
{/* Interval-based fields */}
|
|
{currentScheduleType === 'interval' && (
|
|
<>
|
|
<Grid item xs={12} sm={6}>
|
|
<TextField
|
|
label="Interval (months)"
|
|
type="number"
|
|
fullWidth
|
|
value={formData.intervalMonths || ''}
|
|
onChange={(e) =>
|
|
handleInputChange(
|
|
'intervalMonths',
|
|
e.target.value ? parseInt(e.target.value) : undefined
|
|
)
|
|
}
|
|
helperText="Service every X months"
|
|
inputProps={{ min: 1 }}
|
|
/>
|
|
</Grid>
|
|
<Grid item xs={12} sm={6}>
|
|
<TextField
|
|
label="Interval (miles)"
|
|
type="number"
|
|
fullWidth
|
|
value={formData.intervalMiles || ''}
|
|
onChange={(e) =>
|
|
handleInputChange(
|
|
'intervalMiles',
|
|
e.target.value ? parseInt(e.target.value) : undefined
|
|
)
|
|
}
|
|
helperText="Service every X miles"
|
|
inputProps={{ min: 1 }}
|
|
/>
|
|
</Grid>
|
|
</>
|
|
)}
|
|
|
|
{/* Fixed date field */}
|
|
{currentScheduleType === 'fixed_date' && (
|
|
<Grid item xs={12}>
|
|
<DatePicker
|
|
label="Due Date"
|
|
value={formData.fixedDueDate ? dayjs(String(formData.fixedDueDate).substring(0, 10)) : null}
|
|
onChange={(newValue) =>
|
|
handleInputChange('fixedDueDate', newValue?.format('YYYY-MM-DD') || undefined)
|
|
}
|
|
format="MM/DD/YYYY"
|
|
slotProps={{
|
|
textField: {
|
|
fullWidth: true,
|
|
helperText: 'One-time service due date',
|
|
sx: {
|
|
'& .MuiOutlinedInput-root': {
|
|
minHeight: '56px',
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
/>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Time since last fields */}
|
|
{currentScheduleType === 'time_since_last' && (
|
|
<>
|
|
<Grid item xs={12} sm={6}>
|
|
<TextField
|
|
label="Interval (months)"
|
|
type="number"
|
|
fullWidth
|
|
value={formData.intervalMonths || ''}
|
|
onChange={(e) =>
|
|
handleInputChange(
|
|
'intervalMonths',
|
|
e.target.value ? parseInt(e.target.value) : undefined
|
|
)
|
|
}
|
|
helperText="Months after last service"
|
|
inputProps={{ min: 1 }}
|
|
/>
|
|
</Grid>
|
|
<Grid item xs={12} sm={6}>
|
|
<TextField
|
|
label="Interval (miles)"
|
|
type="number"
|
|
fullWidth
|
|
value={formData.intervalMiles || ''}
|
|
onChange={(e) =>
|
|
handleInputChange(
|
|
'intervalMiles',
|
|
e.target.value ? parseInt(e.target.value) : undefined
|
|
)
|
|
}
|
|
helperText="Miles after last service"
|
|
inputProps={{ min: 1 }}
|
|
/>
|
|
</Grid>
|
|
</>
|
|
)}
|
|
|
|
{/* Email Notifications */}
|
|
<Grid item xs={12}>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={formData.emailNotifications ?? false}
|
|
onChange={(e) => handleInputChange('emailNotifications', e.target.checked)}
|
|
/>
|
|
}
|
|
label="Email notifications"
|
|
/>
|
|
</Grid>
|
|
|
|
{/* Reminder Days (only if email notifications enabled) */}
|
|
{formData.emailNotifications && (
|
|
<>
|
|
<Grid item xs={12}>
|
|
<Typography variant="subtitle2" gutterBottom>
|
|
Reminder Days Before Due Date
|
|
</Typography>
|
|
</Grid>
|
|
<Grid item xs={12} sm={4}>
|
|
<TextField
|
|
label="Reminder 1"
|
|
type="number"
|
|
fullWidth
|
|
value={formData.reminderDays1 || ''}
|
|
onChange={(e) =>
|
|
handleInputChange(
|
|
'reminderDays1',
|
|
e.target.value ? parseInt(e.target.value) : undefined
|
|
)
|
|
}
|
|
helperText="Days before"
|
|
inputProps={{ min: 1 }}
|
|
/>
|
|
</Grid>
|
|
<Grid item xs={12} sm={4}>
|
|
<TextField
|
|
label="Reminder 2"
|
|
type="number"
|
|
fullWidth
|
|
value={formData.reminderDays2 || ''}
|
|
onChange={(e) =>
|
|
handleInputChange(
|
|
'reminderDays2',
|
|
e.target.value ? parseInt(e.target.value) : undefined
|
|
)
|
|
}
|
|
helperText="Days before"
|
|
inputProps={{ min: 1 }}
|
|
/>
|
|
</Grid>
|
|
<Grid item xs={12} sm={4}>
|
|
<TextField
|
|
label="Reminder 3"
|
|
type="number"
|
|
fullWidth
|
|
value={formData.reminderDays3 || ''}
|
|
onChange={(e) =>
|
|
handleInputChange(
|
|
'reminderDays3',
|
|
e.target.value ? parseInt(e.target.value) : undefined
|
|
)
|
|
}
|
|
helperText="Days before"
|
|
inputProps={{ min: 1 }}
|
|
/>
|
|
</Grid>
|
|
</>
|
|
)}
|
|
</Grid>
|
|
</Box>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={handleCancel} disabled={isSaving}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleSave} variant="contained" disabled={isSaving}>
|
|
{isSaving ? 'Saving...' : 'Save Changes'}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</LocalizationProvider>
|
|
);
|
|
};
|