Update
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* @ai-summary Edit dialog for maintenance records
|
||||
* @ai-context Mobile-friendly dialog with proper form handling
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Box,
|
||||
Grid,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
import {
|
||||
MaintenanceRecordResponse,
|
||||
UpdateMaintenanceRecordRequest,
|
||||
MaintenanceCategory,
|
||||
getCategoryDisplayName,
|
||||
} from '../types/maintenance.types';
|
||||
import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup';
|
||||
import { useVehicles } from '../../vehicles/hooks/useVehicles';
|
||||
import type { Vehicle } from '../../vehicles/types/vehicles.types';
|
||||
|
||||
interface MaintenanceRecordEditDialogProps {
|
||||
open: boolean;
|
||||
record: MaintenanceRecordResponse | null;
|
||||
onClose: () => void;
|
||||
onSave: (id: string, data: UpdateMaintenanceRecordRequest) => Promise<void>;
|
||||
}
|
||||
|
||||
export const MaintenanceRecordEditDialog: React.FC<MaintenanceRecordEditDialogProps> = ({
|
||||
open,
|
||||
record,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [formData, setFormData] = useState<UpdateMaintenanceRecordRequest>({});
|
||||
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 record changes
|
||||
useEffect(() => {
|
||||
if (record && record.id) {
|
||||
try {
|
||||
setFormData({
|
||||
category: record.category,
|
||||
subtypes: record.subtypes,
|
||||
date: record.date,
|
||||
odometer_reading: record.odometer_reading || undefined,
|
||||
cost: record.cost ? Number(record.cost) : undefined,
|
||||
shop_name: record.shop_name || undefined,
|
||||
notes: record.notes || undefined,
|
||||
});
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('[MaintenanceRecordEditDialog] Error setting form data:', err);
|
||||
setError(err as Error);
|
||||
}
|
||||
}
|
||||
}, [record]);
|
||||
|
||||
const handleInputChange = (field: keyof UpdateMaintenanceRecordRequest, value: any) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!record || !record.id) {
|
||||
console.error('[MaintenanceRecordEditDialog] No valid record to save');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
|
||||
// Filter out unchanged fields
|
||||
const changedData: UpdateMaintenanceRecordRequest = {};
|
||||
Object.entries(formData).forEach(([key, value]) => {
|
||||
const typedKey = key as keyof UpdateMaintenanceRecordRequest;
|
||||
const recordValue = record[typedKey as keyof MaintenanceRecordResponse];
|
||||
|
||||
// Special handling for arrays
|
||||
if (Array.isArray(value) && Array.isArray(recordValue)) {
|
||||
if (JSON.stringify(value) !== JSON.stringify(recordValue)) {
|
||||
(changedData as any)[key] = value;
|
||||
}
|
||||
} else if (value !== recordValue) {
|
||||
(changedData as any)[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Only send update if there are actual changes
|
||||
if (Object.keys(changedData).length > 0) {
|
||||
await onSave(record.id, changedData);
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
console.error('[MaintenanceRecordEditDialog] Failed to save record:', err);
|
||||
setError(err as Error);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Early bailout if dialog not open or no record to edit
|
||||
if (!open || !record) return null;
|
||||
|
||||
// Error state
|
||||
if (error) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Error Loading Maintenance Record</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography color="error">
|
||||
Failed to load maintenance record 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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LocalizationProvider dateAdapter={AdapterDateFns}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleCancel}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
fullScreen={isSmallScreen}
|
||||
PaperProps={{
|
||||
sx: { maxHeight: '90vh' },
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Edit Maintenance Record</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 === record.vehicle_id);
|
||||
if (!vehicle) return 'Unknown Vehicle';
|
||||
if (vehicle.nickname?.trim()) return vehicle.nickname.trim();
|
||||
const parts = [vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean);
|
||||
return parts.length > 0 ? parts.join(' ') : 'Vehicle';
|
||||
})()}
|
||||
helperText="Vehicle cannot be changed when editing"
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Date */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<DatePicker
|
||||
label="Service Date *"
|
||||
value={formData.date ? new Date(formData.date) : null}
|
||||
onChange={(newValue) =>
|
||||
handleInputChange('date', newValue?.toISOString().split('T')[0] || '')
|
||||
}
|
||||
format="MM/dd/yyyy"
|
||||
slotProps={{
|
||||
textField: {
|
||||
fullWidth: true,
|
||||
sx: {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
minHeight: '56px',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Odometer Reading */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
label="Odometer Reading"
|
||||
type="number"
|
||||
fullWidth
|
||||
value={formData.odometer_reading || ''}
|
||||
onChange={(e) =>
|
||||
handleInputChange(
|
||||
'odometer_reading',
|
||||
e.target.value ? parseInt(e.target.value) : undefined
|
||||
)
|
||||
}
|
||||
helperText="Current mileage"
|
||||
inputProps={{ min: 0 }}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Cost */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
label="Cost"
|
||||
type="number"
|
||||
fullWidth
|
||||
value={formData.cost || ''}
|
||||
onChange={(e) =>
|
||||
handleInputChange('cost', e.target.value ? parseFloat(e.target.value) : undefined)
|
||||
}
|
||||
helperText="Total service cost"
|
||||
inputProps={{ step: 0.01, min: 0 }}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Shop Name */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<TextField
|
||||
label="Shop/Location"
|
||||
fullWidth
|
||||
value={formData.shop_name || ''}
|
||||
onChange={(e) => handleInputChange('shop_name', e.target.value || undefined)}
|
||||
helperText="Service location"
|
||||
inputProps={{ maxLength: 200 }}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Notes */}
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
label="Notes"
|
||||
multiline
|
||||
rows={3}
|
||||
fullWidth
|
||||
value={formData.notes || ''}
|
||||
onChange={(e) => handleInputChange('notes', e.target.value || undefined)}
|
||||
placeholder="Optional notes about this service..."
|
||||
inputProps={{ maxLength: 10000 }}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user