UX Improvements
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Typography, Button, Card, CardContent, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem, ListItemText } from '@mui/material';
|
||||
import { Box, Typography, Button, Card, CardContent, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem } from '@mui/material';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Vehicle } from '../types/vehicles.types';
|
||||
import { useFuelLogs } from '../../fuel-logs/hooks/useFuelLogs';
|
||||
@@ -57,6 +57,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
||||
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(vehicle.id);
|
||||
const queryClient = useQueryClient();
|
||||
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
|
||||
// Unit conversions are now handled by the backend
|
||||
|
||||
type VehicleRecord = {
|
||||
id: string;
|
||||
@@ -64,19 +65,71 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
||||
date: string; // ISO
|
||||
summary: string;
|
||||
amount?: string;
|
||||
secondary?: string;
|
||||
};
|
||||
|
||||
const records: VehicleRecord[] = useMemo(() => {
|
||||
const list: VehicleRecord[] = [];
|
||||
if (fuelLogs && Array.isArray(fuelLogs)) {
|
||||
// Build a map of prior odometer readings to compute trip distance when missing
|
||||
const logsAsc = [...(fuelLogs as FuelLogResponse[])].sort(
|
||||
(a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime()
|
||||
);
|
||||
const prevOdoById = new Map<string, number | undefined>();
|
||||
let lastOdo: number | undefined = undefined;
|
||||
for (const l of logsAsc) {
|
||||
prevOdoById.set(l.id, lastOdo);
|
||||
if (typeof l.odometerReading === 'number' && !isNaN(l.odometerReading)) {
|
||||
lastOdo = l.odometerReading;
|
||||
}
|
||||
}
|
||||
|
||||
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(' • ');
|
||||
// Use efficiency from API response (backend calculates this properly)
|
||||
let efficiency = '';
|
||||
if (typeof log.efficiency === 'number' && log.efficiency > 0) {
|
||||
// DEBUG: Log what the backend is actually returning
|
||||
console.log('🔍 Fuel Log API Response:', {
|
||||
efficiency: log.efficiency,
|
||||
efficiencyLabel: log.efficiencyLabel,
|
||||
logId: log.id
|
||||
});
|
||||
// Backend returns efficiency in correct units based on user preference
|
||||
efficiency = `${log.efficiencyLabel || 'MPG'}: ${log.efficiency.toFixed(3)}`;
|
||||
}
|
||||
|
||||
// Secondary line components
|
||||
const secondaryParts: string[] = [];
|
||||
|
||||
// Grade label (prefer grade only)
|
||||
if (log.fuelGrade) {
|
||||
secondaryParts.push(`Grade: ${log.fuelGrade}`);
|
||||
} else if (log.fuelType) {
|
||||
const ft = String(log.fuelType);
|
||||
secondaryParts.push(ft.charAt(0).toUpperCase() + ft.slice(1));
|
||||
}
|
||||
|
||||
// Date
|
||||
secondaryParts.push(new Date(log.dateTime).toLocaleDateString());
|
||||
|
||||
// Type
|
||||
secondaryParts.push('Fuel Log');
|
||||
|
||||
const summary = efficiency; // Primary line shows MPG/km/L from backend
|
||||
const secondary = secondaryParts.join(' • '); // Secondary line shows Grade • Date • Type
|
||||
const amount = (typeof log.totalCost === 'number') ? `$${log.totalCost.toFixed(2)}` : undefined;
|
||||
list.push({ id: log.id, type: 'Fuel Logs', date: log.dateTime, summary, amount });
|
||||
|
||||
// DEBUG: Log the final display values
|
||||
console.log('🎯 Display Summary:', { summary, efficiency, logId: log.id });
|
||||
|
||||
list.push({
|
||||
id: log.id,
|
||||
type: 'Fuel Logs',
|
||||
date: log.dateTime,
|
||||
summary,
|
||||
amount,
|
||||
secondary
|
||||
});
|
||||
}
|
||||
}
|
||||
return list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
||||
@@ -212,13 +265,21 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
||||
<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>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
{/* Primary line: MPG/km-L and amount */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{rec.summary || 'MPG: —'}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.primary">
|
||||
{rec.amount || '—'}
|
||||
</Typography>
|
||||
</Box>
|
||||
{/* Secondary line: Grade • Date • Type */}
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.25 }}>
|
||||
{rec.secondary || `${new Date(rec.date).toLocaleDateString()} • ${rec.type}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Box, Typography, Button as MuiButton, Divider, FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableCell, TableBody } from '@mui/material';
|
||||
import { Box, Typography, Button as MuiButton, Divider, FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableCell, TableBody, Dialog, DialogTitle, DialogContent, useMediaQuery } from '@mui/material';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
@@ -17,6 +17,8 @@ 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 { FuelLogForm } from '../../fuel-logs/components/FuelLogForm';
|
||||
// Unit conversions now handled by backend
|
||||
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
|
||||
|
||||
const DetailField: React.FC<{
|
||||
@@ -49,6 +51,9 @@ export const VehicleDetailPage: React.FC = () => {
|
||||
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(id);
|
||||
const queryClient = useQueryClient();
|
||||
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const isSmallScreen = useMediaQuery('(max-width:600px)');
|
||||
// Unit conversions now handled by backend
|
||||
|
||||
// Define records list hooks BEFORE any early returns to keep hooks order stable
|
||||
type VehicleRecord = {
|
||||
@@ -62,11 +67,35 @@ export const VehicleDetailPage: React.FC = () => {
|
||||
const records: VehicleRecord[] = useMemo(() => {
|
||||
const list: VehicleRecord[] = [];
|
||||
if (fuelLogs && Array.isArray(fuelLogs)) {
|
||||
// Build a map of prior odometer readings to compute trip distance when missing
|
||||
const logsAsc = [...(fuelLogs as FuelLogResponse[])].sort(
|
||||
(a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime()
|
||||
);
|
||||
const prevOdoById = new Map<string, number | undefined>();
|
||||
let lastOdo: number | undefined = undefined;
|
||||
for (const l of logsAsc) {
|
||||
prevOdoById.set(l.id, lastOdo);
|
||||
if (typeof l.odometerReading === 'number' && !isNaN(l.odometerReading)) {
|
||||
lastOdo = l.odometerReading;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Efficiency: Use backend calculation (primary display)
|
||||
if (typeof log.efficiency === 'number' && log.efficiency > 0) {
|
||||
parts.push(`${log.efficiencyLabel || 'MPG'}: ${log.efficiency.toFixed(3)}`);
|
||||
}
|
||||
|
||||
// Grade label (secondary display)
|
||||
if (log.fuelGrade) {
|
||||
parts.push(`Grade: ${log.fuelGrade}`);
|
||||
} else if (log.fuelType) {
|
||||
const ft = String(log.fuelType);
|
||||
parts.push(ft.charAt(0).toUpperCase() + ft.slice(1));
|
||||
}
|
||||
|
||||
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 });
|
||||
@@ -240,10 +269,11 @@ export const VehicleDetailPage: React.FC = () => {
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 4 }}>
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
<MuiButton
|
||||
variant="contained"
|
||||
startIcon={<LocalGasStationIcon />}
|
||||
sx={{ borderRadius: '999px' }}
|
||||
onClick={() => setShowAddDialog(true)}
|
||||
>
|
||||
Add Fuel Log
|
||||
</MuiButton>
|
||||
@@ -343,7 +373,7 @@ export const VehicleDetailPage: React.FC = () => {
|
||||
)}
|
||||
{!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>{new Date(rec.date).toLocaleDateString()}</TableCell>
|
||||
<TableCell>{rec.type}</TableCell>
|
||||
<TableCell>{rec.summary}</TableCell>
|
||||
<TableCell align="right">{rec.amount || '—'}</TableCell>
|
||||
@@ -360,6 +390,33 @@ export const VehicleDetailPage: React.FC = () => {
|
||||
onClose={handleCloseEdit}
|
||||
onSave={handleSaveEdit}
|
||||
/>
|
||||
|
||||
{/* Add Fuel Log Dialog */}
|
||||
<Dialog
|
||||
open={showAddDialog}
|
||||
onClose={() => setShowAddDialog(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
fullScreen={isSmallScreen}
|
||||
PaperProps={{
|
||||
sx: { maxHeight: '90vh' }
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Add Fuel Log</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<FuelLogForm
|
||||
initial={{ vehicleId: vehicle?.id }}
|
||||
onSuccess={() => {
|
||||
setShowAddDialog(false);
|
||||
// Refresh fuel logs data
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogs', vehicle?.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user