Files
motovaultpro/frontend/src/features/maintenance/components/MaintenanceScheduleEditDialog.tsx
Eric Gullickson f0fc427ccd
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
fix: Date picker bug
2026-03-23 20:03:49 -05:00

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>
);
};