Record UX improvements
This commit is contained in:
@@ -35,15 +35,11 @@ export const FuelLogEditDialog: React.FC<FuelLogEditDialogProps> = ({
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [hookError, setHookError] = useState<Error | null>(null);
|
||||
|
||||
// Defensive hook usage with error handling
|
||||
let fuelGrades: any[] = [];
|
||||
try {
|
||||
const hookResult = useFuelGrades(formData.fuelType || log?.fuelType || FuelType.GASOLINE);
|
||||
fuelGrades = hookResult.fuelGrades || [];
|
||||
} catch (error) {
|
||||
console.error('[FuelLogEditDialog] Hook error:', error);
|
||||
setHookError(error as Error);
|
||||
}
|
||||
// Always call hooks at the top level to maintain order across renders
|
||||
const { fuelGrades } = useFuelGrades(
|
||||
(formData.fuelType as any) || (log?.fuelType as any) || FuelType.GASOLINE
|
||||
);
|
||||
const isSmallScreen = useMediaQuery('(max-width:600px)');
|
||||
|
||||
// Reset form when log changes with defensive checks
|
||||
useEffect(() => {
|
||||
@@ -111,9 +107,10 @@ export const FuelLogEditDialog: React.FC<FuelLogEditDialogProps> = ({
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Early returns for error states
|
||||
if (!log) return null;
|
||||
// Early bailout if dialog not open or no log to edit
|
||||
if (!open || !log) return null;
|
||||
|
||||
// Early returns for error states
|
||||
if (hookError) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
@@ -150,7 +147,7 @@ export const FuelLogEditDialog: React.FC<FuelLogEditDialogProps> = ({
|
||||
onClose={handleCancel}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
fullScreen={useMediaQuery('(max-width:600px)')}
|
||||
fullScreen={isSmallScreen}
|
||||
PaperProps={{
|
||||
sx: { maxHeight: '90vh' }
|
||||
}}
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
* @ai-summary Mobile vehicle detail screen with Material Design 3
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Typography, Button, Card, CardContent, Divider } from '@mui/material';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Typography, Button, Card, CardContent, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem, ListItemText } from '@mui/material';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Vehicle } from '../types/vehicles.types';
|
||||
import { useFuelLogs } from '../../fuel-logs/hooks/useFuelLogs';
|
||||
import { FuelLogResponse, UpdateFuelLogRequest } from '../../fuel-logs/types/fuel-logs.types';
|
||||
import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog';
|
||||
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
|
||||
|
||||
// Theme colors now defined in Tailwind config
|
||||
|
||||
@@ -48,6 +53,55 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
||||
[vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean).join(' ') || 'Vehicle';
|
||||
const displayModel = vehicle.model || 'Unknown Model';
|
||||
|
||||
const [recordFilter, setRecordFilter] = useState<'All' | 'Fuel Logs' | 'Maintenance' | 'Documents'>('All');
|
||||
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(vehicle.id);
|
||||
const queryClient = useQueryClient();
|
||||
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
|
||||
|
||||
type VehicleRecord = {
|
||||
id: string;
|
||||
type: 'Fuel Logs' | 'Maintenance' | 'Documents';
|
||||
date: string; // ISO
|
||||
summary: string;
|
||||
amount?: string;
|
||||
};
|
||||
|
||||
const records: VehicleRecord[] = useMemo(() => {
|
||||
const list: VehicleRecord[] = [];
|
||||
if (fuelLogs && Array.isArray(fuelLogs)) {
|
||||
for (const log of fuelLogs as FuelLogResponse[]) {
|
||||
const parts: string[] = [];
|
||||
if (log.fuelUnits) parts.push(`${Number(log.fuelUnits).toFixed(3)} units`);
|
||||
if (log.fuelType) parts.push(`${log.fuelType}${log.fuelGrade ? ' ' + log.fuelGrade : ''}`);
|
||||
if (log.efficiencyLabel) parts.push(log.efficiencyLabel);
|
||||
const summary = parts.join(' • ');
|
||||
const amount = (typeof log.totalCost === 'number') ? `$${log.totalCost.toFixed(2)}` : undefined;
|
||||
list.push({ id: log.id, type: 'Fuel Logs', date: log.dateTime, summary, amount });
|
||||
}
|
||||
}
|
||||
return list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
}, [fuelLogs]);
|
||||
|
||||
const filteredRecords = useMemo(() => {
|
||||
if (recordFilter === 'All') return records;
|
||||
return records.filter(r => r.type === recordFilter);
|
||||
}, [records, recordFilter]);
|
||||
|
||||
const openEditLog = (recId: string, type: VehicleRecord['type']) => {
|
||||
if (type === 'Fuel Logs') {
|
||||
const log = (fuelLogs as FuelLogResponse[] | undefined)?.find(l => l.id === recId) || null;
|
||||
setEditingLog(log);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseEdit = () => setEditingLog(null);
|
||||
const handleSaveEdit = async (id: string, data: UpdateFuelLogRequest) => {
|
||||
await fuelLogsApi.update(id, data);
|
||||
await queryClient.invalidateQueries({ queryKey: ['fuelLogs', vehicle.id] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
||||
setEditingLog(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ pb: 10 }}>
|
||||
<Button variant="text" onClick={onBack}>
|
||||
@@ -124,15 +178,62 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
<Section title="Recent Activity">
|
||||
<Section title="Vehicle Records">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{isFuelLoading ? 'Loading…' : `${filteredRecords.length} record${filteredRecords.length === 1 ? '' : 's'}`}
|
||||
</Typography>
|
||||
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||
<InputLabel id="vehicle-records-filter">Filter</InputLabel>
|
||||
<Select
|
||||
labelId="vehicle-records-filter"
|
||||
value={recordFilter}
|
||||
label="Filter"
|
||||
onChange={(e) => setRecordFilter(e.target.value as any)}
|
||||
>
|
||||
<MenuItem value="All">All</MenuItem>
|
||||
<MenuItem value="Fuel Logs">Fuel Logs</MenuItem>
|
||||
<MenuItem value="Maintenance">Maintenance</MenuItem>
|
||||
<MenuItem value="Documents">Documents</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Card>
|
||||
<CardContent sx={{ textAlign: 'center', py: 8 }}>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
No recent activity
|
||||
</Typography>
|
||||
<CardContent sx={{ p: 0 }}>
|
||||
{isFuelLoading ? (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography color="text.secondary" variant="body2">Loading records…</Typography>
|
||||
</Box>
|
||||
) : filteredRecords.length === 0 ? (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography color="text.secondary" variant="body2">No records found for this filter.</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<List disablePadding>
|
||||
{filteredRecords.map(rec => (
|
||||
<ListItem key={rec.id} divider button onClick={() => openEditLog(rec.id, rec.type)}>
|
||||
<ListItemText
|
||||
primary={rec.summary || new Date(rec.date).toLocaleString()}
|
||||
secondary={`${new Date(rec.date).toLocaleString()} • ${rec.type}`}
|
||||
/>
|
||||
<Typography sx={{ ml: 2 }} color="text.primary">
|
||||
{rec.amount || '—'}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Section>
|
||||
|
||||
{/* Edit Dialog for Fuel Logs */}
|
||||
<FuelLogEditDialog
|
||||
open={!!editingLog}
|
||||
log={editingLog}
|
||||
onClose={handleCloseEdit}
|
||||
onSave={handleSaveEdit}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -2,9 +2,10 @@
|
||||
* @ai-summary Vehicle detail page matching VehicleForm styling
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Box, Typography, Button as MuiButton, Divider } from '@mui/material';
|
||||
import { Box, Typography, Button as MuiButton, Divider, FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableCell, TableBody } from '@mui/material';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation';
|
||||
@@ -13,6 +14,10 @@ import { Vehicle } from '../types/vehicles.types';
|
||||
import { vehiclesApi } from '../api/vehicles.api';
|
||||
import { Card } from '../../../shared-minimal/components/Card';
|
||||
import { VehicleForm } from '../components/VehicleForm';
|
||||
import { useFuelLogs } from '../../fuel-logs/hooks/useFuelLogs';
|
||||
import { FuelLogResponse, UpdateFuelLogRequest } from '../../fuel-logs/types/fuel-logs.types';
|
||||
import { FuelLogEditDialog } from '../../fuel-logs/components/FuelLogEditDialog';
|
||||
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
|
||||
|
||||
const DetailField: React.FC<{
|
||||
label: string;
|
||||
@@ -39,6 +44,41 @@ export const VehicleDetailPage: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [recordFilter, setRecordFilter] = useState<'All' | 'Fuel Logs' | 'Maintenance' | 'Documents'>('All');
|
||||
|
||||
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(id);
|
||||
const queryClient = useQueryClient();
|
||||
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
|
||||
|
||||
// Define records list hooks BEFORE any early returns to keep hooks order stable
|
||||
type VehicleRecord = {
|
||||
id: string;
|
||||
type: 'Fuel Logs' | 'Maintenance' | 'Documents';
|
||||
date: string; // ISO
|
||||
summary: string;
|
||||
amount?: string; // formatted
|
||||
};
|
||||
|
||||
const records: VehicleRecord[] = useMemo(() => {
|
||||
const list: VehicleRecord[] = [];
|
||||
if (fuelLogs && Array.isArray(fuelLogs)) {
|
||||
for (const log of fuelLogs as FuelLogResponse[]) {
|
||||
const parts: string[] = [];
|
||||
if (log.fuelUnits) parts.push(`${Number(log.fuelUnits).toFixed(3)} units`);
|
||||
if (log.fuelType) parts.push(`${log.fuelType}${log.fuelGrade ? ' ' + log.fuelGrade : ''}`);
|
||||
if (log.efficiencyLabel) parts.push(log.efficiencyLabel);
|
||||
const summary = parts.join(' • ');
|
||||
const amount = (typeof log.totalCost === 'number') ? `$${log.totalCost.toFixed(2)}` : undefined;
|
||||
list.push({ id: log.id, type: 'Fuel Logs', date: log.dateTime, summary, amount });
|
||||
}
|
||||
}
|
||||
return list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
}, [fuelLogs]);
|
||||
|
||||
const filteredRecords = useMemo(() => {
|
||||
if (recordFilter === 'All') return records;
|
||||
return records.filter(r => r.type === recordFilter);
|
||||
}, [records, recordFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadVehicle = async () => {
|
||||
@@ -120,6 +160,22 @@ export const VehicleDetailPage: React.FC = () => {
|
||||
const displayName = vehicle.nickname ||
|
||||
[vehicle.year, vehicle.make, vehicle.model, vehicle.trimLevel].filter(Boolean).join(' ') || 'Vehicle';
|
||||
|
||||
const handleRowClick = (recId: string, type: VehicleRecord['type']) => {
|
||||
if (type === 'Fuel Logs') {
|
||||
const log = (fuelLogs as FuelLogResponse[] | undefined)?.find(l => l.id === recId) || null;
|
||||
setEditingLog(log);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseEdit = () => setEditingLog(null);
|
||||
const handleSaveEdit = async (id: string, data: UpdateFuelLogRequest) => {
|
||||
await fuelLogsApi.update(id, data);
|
||||
await queryClient.invalidateQueries({ queryKey: ['fuelLogs', id] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['fuelLogs', vehicle?.id] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
||||
setEditingLog(null);
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<Box sx={{ py: 2 }}>
|
||||
@@ -240,15 +296,70 @@ export const VehicleDetailPage: React.FC = () => {
|
||||
|
||||
<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 sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
Vehicle Records
|
||||
</Typography>
|
||||
<FormControl size="small" sx={{ minWidth: 220 }}>
|
||||
<InputLabel id="record-filter-label">Filter</InputLabel>
|
||||
<Select
|
||||
labelId="record-filter-label"
|
||||
value={recordFilter}
|
||||
label="Filter"
|
||||
onChange={(e) => setRecordFilter(e.target.value as any)}
|
||||
>
|
||||
<MenuItem value="All">All</MenuItem>
|
||||
<MenuItem value="Fuel Logs">Fuel Logs</MenuItem>
|
||||
<MenuItem value="Maintenance">Maintenance</MenuItem>
|
||||
<MenuItem value="Documents">Documents</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ width: 200 }}>Date</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Summary</TableCell>
|
||||
<TableCell align="right">Amount</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isFuelLoading && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4}>
|
||||
<Typography color="text.secondary">Loading records…</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{!isFuelLoading && filteredRecords.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4}>
|
||||
<Typography color="text.secondary">No records found for this filter.</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{!isFuelLoading && filteredRecords.map((rec) => (
|
||||
<TableRow key={rec.id} hover sx={{ cursor: 'pointer' }} onClick={() => handleRowClick(rec.id, rec.type)}>
|
||||
<TableCell>{new Date(rec.date).toLocaleString()}</TableCell>
|
||||
<TableCell>{rec.type}</TableCell>
|
||||
<TableCell>{rec.summary}</TableCell>
|
||||
<TableCell align="right">{rec.amount || '—'}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
{/* Edit Dialog for Fuel Logs */}
|
||||
<FuelLogEditDialog
|
||||
open={!!editingLog}
|
||||
log={editingLog}
|
||||
onClose={handleCloseEdit}
|
||||
onSave={handleSaveEdit}
|
||||
/>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user