Initial Commit
This commit is contained in:
255
frontend/src/features/vehicles/pages/VehicleDetailPage.tsx
Normal file
255
frontend/src/features/vehicles/pages/VehicleDetailPage.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* @ai-summary Vehicle detail page matching VehicleForm styling
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Box, Typography, Button as MuiButton, Divider } from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import { Vehicle } from '../types/vehicles.types';
|
||||
import { vehiclesApi } from '../api/vehicles.api';
|
||||
import { Card } from '../../../shared-minimal/components/Card';
|
||||
import { VehicleForm } from '../components/VehicleForm';
|
||||
|
||||
const DetailField: React.FC<{
|
||||
label: string;
|
||||
value?: string | number;
|
||||
isRequired?: boolean;
|
||||
className?: string;
|
||||
}> = ({ label, value, isRequired, className = "" }) => (
|
||||
<div className={`space-y-1 ${className}`}>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
{label} {isRequired && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
<div className="px-3 py-2 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<span className="text-gray-900">
|
||||
{value || <span className="text-gray-400 italic">Not provided</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const VehicleDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [vehicle, setVehicle] = useState<Vehicle | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadVehicle = async () => {
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const vehicleData = await vehiclesApi.getById(id);
|
||||
setVehicle(vehicleData);
|
||||
} catch (err) {
|
||||
setError('Failed to load vehicle details');
|
||||
console.error('Error loading vehicle:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadVehicle();
|
||||
}, [id]);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate('/vehicles');
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleUpdateVehicle = async (data: any) => {
|
||||
if (!vehicle) return;
|
||||
|
||||
try {
|
||||
const updatedVehicle = await vehiclesApi.update(vehicle.id, data);
|
||||
setVehicle(updatedVehicle);
|
||||
setIsEditing(false);
|
||||
} catch (err) {
|
||||
console.error('Error updating vehicle:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '50vh'
|
||||
}}>
|
||||
<Typography color="text.secondary">Loading vehicle details...</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !vehicle) {
|
||||
return (
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Card>
|
||||
<Box sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Typography color="error.main" sx={{ mb: 3 }}>
|
||||
{error || 'Vehicle not found'}
|
||||
</Typography>
|
||||
<MuiButton
|
||||
variant="outlined"
|
||||
onClick={handleBack}
|
||||
startIcon={<ArrowBackIcon />}
|
||||
>
|
||||
Back to Vehicles
|
||||
</MuiButton>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const displayName = vehicle.nickname ||
|
||||
[vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean).join(' ') || 'Vehicle';
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mb: 4
|
||||
}}>
|
||||
<MuiButton
|
||||
variant="text"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={handleCancelEdit}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Cancel
|
||||
</MuiButton>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||
Edit {displayName}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Card>
|
||||
<VehicleForm
|
||||
initialData={vehicle}
|
||||
onSubmit={handleUpdateVehicle}
|
||||
onCancel={handleCancelEdit}
|
||||
/>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ py: 2 }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 4
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<MuiButton
|
||||
variant="text"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Back
|
||||
</MuiButton>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||
{displayName}
|
||||
</Typography>
|
||||
</Box>
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
startIcon={<EditIcon />}
|
||||
onClick={handleEdit}
|
||||
sx={{ borderRadius: '999px' }}
|
||||
>
|
||||
Edit Vehicle
|
||||
</MuiButton>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 4 }}>
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
startIcon={<LocalGasStationIcon />}
|
||||
sx={{ borderRadius: '999px' }}
|
||||
>
|
||||
Add Fuel Log
|
||||
</MuiButton>
|
||||
<MuiButton
|
||||
variant="outlined"
|
||||
startIcon={<BuildIcon />}
|
||||
sx={{ borderRadius: '999px' }}
|
||||
>
|
||||
Schedule Maintenance
|
||||
</MuiButton>
|
||||
</Box>
|
||||
|
||||
<Card>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
|
||||
Vehicle Details
|
||||
</Typography>
|
||||
|
||||
<form className="space-y-4">
|
||||
<DetailField
|
||||
label="VIN or License Plate"
|
||||
value={vehicle.vin || vehicle.licensePlate}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
{/* Vehicle Specification Section */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<DetailField label="Year" value={vehicle.year} />
|
||||
<DetailField label="Make" value={vehicle.make} />
|
||||
<DetailField label="Model" value={vehicle.model} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<DetailField label="Trim" value={vehicle.trimLevel} />
|
||||
<DetailField label="Engine" value={vehicle.engine} />
|
||||
<DetailField label="Transmission" value={vehicle.transmission} />
|
||||
</div>
|
||||
|
||||
<DetailField label="Nickname" value={vehicle.nickname} />
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<DetailField label="Color" value={vehicle.color} />
|
||||
<DetailField label="License Plate" value={vehicle.licensePlate} />
|
||||
</div>
|
||||
|
||||
<DetailField
|
||||
label="Current Odometer Reading"
|
||||
value={vehicle.odometerReading ? `${vehicle.odometerReading.toLocaleString()} mi` : undefined}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Divider sx={{ my: 4 }} />
|
||||
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
|
||||
Vehicle Information
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 4, color: 'text.secondary', fontSize: '0.875rem' }}>
|
||||
<span>Added: {new Date(vehicle.createdAt).toLocaleDateString()}</span>
|
||||
{vehicle.updatedAt !== vehicle.createdAt && (
|
||||
<span>Last updated: {new Date(vehicle.updatedAt).toLocaleDateString()}</span>
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user