Fix Auth Errors

This commit is contained in:
Eric Gullickson
2025-09-22 10:27:10 -05:00
parent 3588372cef
commit 8fd7973656
19 changed files with 1342 additions and 174 deletions

View File

@@ -1,5 +1,5 @@
import { apiClient } from '../../../core/api/client';
import { CreateFuelLogRequest, FuelLogResponse, EnhancedFuelStats, FuelType, FuelGradeOption } from '../types/fuel-logs.types';
import { CreateFuelLogRequest, UpdateFuelLogRequest, FuelLogResponse, EnhancedFuelStats, FuelType, FuelGradeOption } from '../types/fuel-logs.types';
export const fuelLogsApi = {
async create(data: CreateFuelLogRequest): Promise<FuelLogResponse> {
@@ -30,6 +30,20 @@ export const fuelLogsApi = {
async getFuelGrades(fuelType: FuelType): Promise<FuelGradeOption[]> {
const res = await apiClient.get(`/fuel-logs/fuel-grades/${fuelType}`);
return res.data.grades;
},
async update(id: string, data: UpdateFuelLogRequest): Promise<FuelLogResponse> {
const res = await apiClient.put(`/fuel-logs/${id}`, data);
return res.data;
},
async delete(id: string): Promise<void> {
await apiClient.delete(`/fuel-logs/${id}`);
},
async getById(id: string): Promise<FuelLogResponse> {
const res = await apiClient.get(`/fuel-logs/${id}`);
return res.data;
}
};

View File

@@ -0,0 +1,291 @@
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 { FuelLogResponse, UpdateFuelLogRequest, FuelType } from '../types/fuel-logs.types';
import { useFuelGrades } from '../hooks/useFuelGrades';
interface FuelLogEditDialogProps {
open: boolean;
log: FuelLogResponse | null;
onClose: () => void;
onSave: (id: string, data: UpdateFuelLogRequest) => Promise<void>;
}
export const FuelLogEditDialog: React.FC<FuelLogEditDialogProps> = ({
open,
log,
onClose,
onSave
}) => {
const [formData, setFormData] = useState<UpdateFuelLogRequest>({});
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);
}
// Reset form when log changes with defensive checks
useEffect(() => {
if (log && log.id) {
try {
setFormData({
dateTime: log.dateTime || new Date().toISOString(),
odometerReading: log.odometerReading || undefined,
tripDistance: log.tripDistance || undefined,
fuelType: log.fuelType || FuelType.GASOLINE,
fuelGrade: log.fuelGrade || null,
fuelUnits: log.fuelUnits || 0,
costPerUnit: log.costPerUnit || 0,
notes: log.notes || ''
});
setHookError(null); // Reset any previous errors
} catch (error) {
console.error('[FuelLogEditDialog] Error setting form data:', error);
setHookError(error as Error);
}
}
}, [log]);
const handleInputChange = (field: keyof UpdateFuelLogRequest, value: any) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const handleSave = async () => {
if (!log || !log.id) {
console.error('[FuelLogEditDialog] No valid log to save');
return;
}
try {
setIsSaving(true);
// Filter out unchanged fields with defensive checks
const changedData: UpdateFuelLogRequest = {};
Object.entries(formData).forEach(([key, value]) => {
const typedKey = key as keyof UpdateFuelLogRequest;
if (value !== log[typedKey as keyof FuelLogResponse]) {
(changedData as any)[key] = value;
}
});
// Only send update if there are actual changes
if (Object.keys(changedData).length > 0) {
await onSave(log.id, changedData);
}
onClose();
} catch (error) {
console.error('[FuelLogEditDialog] Failed to save fuel log:', error);
setHookError(error as Error);
// Don't close dialog on error, let user retry
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
onClose();
};
// Early returns for error states
if (!log) return null;
if (hookError) {
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Error Loading Fuel Log</DialogTitle>
<DialogContent>
<Typography color="error">
Failed to load fuel log data. Please try again.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{hookError.message}
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Close</Button>
</DialogActions>
</Dialog>
);
}
// Format datetime for input (datetime-local expects YYYY-MM-DDTHH:mm format)
const formatDateTimeForInput = (isoString: string) => {
const date = new Date(isoString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
};
return (
<Dialog
open={open}
onClose={handleCancel}
maxWidth="sm"
fullWidth
fullScreen={useMediaQuery('(max-width:600px)')}
PaperProps={{
sx: { maxHeight: '90vh' }
}}
>
<DialogTitle>Edit Fuel Log</DialogTitle>
<DialogContent>
<Box sx={{ mt: 1 }}>
<Grid container spacing={2}>
{/* Date and Time */}
<Grid item xs={12}>
<TextField
label="Date & Time"
type="datetime-local"
fullWidth
value={formData.dateTime ? formatDateTimeForInput(formData.dateTime) : ''}
onChange={(e) => handleInputChange('dateTime', new Date(e.target.value).toISOString())}
InputLabelProps={{ shrink: true }}
/>
</Grid>
{/* Distance Inputs */}
<Grid item xs={6}>
<TextField
label="Odometer Reading"
type="number"
fullWidth
value={formData.odometerReading || ''}
onChange={(e) => handleInputChange('odometerReading', e.target.value ? parseFloat(e.target.value) : undefined)}
helperText="Current odometer reading"
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Trip Distance"
type="number"
fullWidth
value={formData.tripDistance || ''}
onChange={(e) => handleInputChange('tripDistance', e.target.value ? parseFloat(e.target.value) : undefined)}
helperText="Distance for this trip"
/>
</Grid>
{/* Fuel Type */}
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>Fuel Type</InputLabel>
<Select
value={formData.fuelType || ''}
onChange={(e) => handleInputChange('fuelType', e.target.value as FuelType)}
label="Fuel Type"
>
<MenuItem value={FuelType.GASOLINE}>Gasoline</MenuItem>
<MenuItem value={FuelType.DIESEL}>Diesel</MenuItem>
<MenuItem value={FuelType.ELECTRIC}>Electric</MenuItem>
</Select>
</FormControl>
</Grid>
{/* Fuel Grade */}
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>Fuel Grade</InputLabel>
<Select
value={formData.fuelGrade || ''}
onChange={(e) => handleInputChange('fuelGrade', e.target.value || null)}
label="Fuel Grade"
disabled={!fuelGrades || fuelGrades.length === 0}
>
{fuelGrades?.map((grade) => (
<MenuItem key={grade.value || 'none'} value={grade.value || ''}>
{grade.label}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
{/* Fuel Amount and Cost */}
<Grid item xs={6}>
<TextField
label="Fuel Amount"
type="number"
fullWidth
value={formData.fuelUnits || ''}
onChange={(e) => handleInputChange('fuelUnits', e.target.value ? parseFloat(e.target.value) : undefined)}
helperText="Gallons or liters"
inputProps={{ step: 0.001 }}
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Cost Per Unit"
type="number"
fullWidth
value={formData.costPerUnit || ''}
onChange={(e) => handleInputChange('costPerUnit', e.target.value ? parseFloat(e.target.value) : undefined)}
helperText="Price per gallon/liter"
inputProps={{ step: 0.001 }}
/>
</Grid>
{/* Total Cost Display */}
<Grid item xs={12}>
<Typography variant="body2" color="text.secondary">
Total Cost: ${((formData.fuelUnits || 0) * (formData.costPerUnit || 0)).toFixed(2)}
</Typography>
</Grid>
{/* Notes */}
<Grid item xs={12}>
<TextField
label="Notes"
multiline
rows={3}
fullWidth
value={formData.notes || ''}
onChange={(e) => handleInputChange('notes', e.target.value)}
placeholder="Optional notes about this fuel-up..."
/>
</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>
);
};

View File

@@ -1,27 +1,241 @@
import React from 'react';
import { Card, CardContent, Typography, List, ListItem, ListItemText, Chip, Box } from '@mui/material';
import React, { useState } from 'react';
import {
Card,
CardContent,
Typography,
List,
ListItem,
ListItemText,
Chip,
Box,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
useTheme,
useMediaQuery
} from '@mui/material';
import { Edit, Delete } from '@mui/icons-material';
import { FuelLogResponse } from '../types/fuel-logs.types';
import { fuelLogsApi } from '../api/fuel-logs.api';
export const FuelLogsList: React.FC<{ logs?: FuelLogResponse[] }>= ({ logs }) => {
if (!logs || logs.length === 0) {
interface FuelLogsListProps {
logs?: FuelLogResponse[];
onEdit?: (log: FuelLogResponse) => void;
onDelete?: (logId: string) => void;
}
export const FuelLogsList: React.FC<FuelLogsListProps> = ({ logs, onEdit, onDelete }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [logToDelete, setLogToDelete] = useState<FuelLogResponse | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const handleDeleteClick = (log: FuelLogResponse) => {
setLogToDelete(log);
setDeleteDialogOpen(true);
};
const handleDeleteConfirm = async () => {
if (!logToDelete) return;
try {
setIsDeleting(true);
await fuelLogsApi.delete(logToDelete.id);
onDelete?.(logToDelete.id);
setDeleteDialogOpen(false);
setLogToDelete(null);
} catch (error) {
console.error('Failed to delete fuel log:', error);
// TODO: Show error notification
} finally {
setIsDeleting(false);
}
};
const handleDeleteCancel = () => {
setDeleteDialogOpen(false);
setLogToDelete(null);
};
// Defensive check for logs data
if (!Array.isArray(logs) || logs.length === 0) {
return (
<Card variant="outlined"><CardContent><Typography variant="body2" color="text.secondary">No fuel logs yet.</Typography></CardContent></Card>
<Card variant="outlined">
<CardContent>
<Typography variant="body2" color="text.secondary">
No fuel logs yet.
</Typography>
</CardContent>
</Card>
);
}
return (
<List>
{logs.map((log) => (
<ListItem key={log.id} divider>
<ListItemText
primary={`${new Date(log.dateTime).toLocaleString()} $${(log.totalCost || 0).toFixed(2)}`}
secondary={`${(log.fuelUnits || 0).toFixed(3)} @ $${(log.costPerUnit || 0).toFixed(3)}${log.odometerReading ? `Odo: ${log.odometerReading}` : `Trip: ${log.tripDistance}`}`}
/>
{log.efficiency && typeof log.efficiency === 'number' && !isNaN(log.efficiency) && (
<Box><Chip label={`${log.efficiency.toFixed(1)} ${log.efficiencyLabel}`} size="small" color="primary" /></Box>
<>
<List>
{logs.map((log) => {
// Defensive checks for each log entry
if (!log || !log.id) {
console.warn('[FuelLogsList] Invalid log entry:', log);
return null;
}
try {
// Safe date formatting
const dateText = log.dateTime
? new Date(log.dateTime).toLocaleString()
: 'Unknown date';
// Safe cost formatting
const totalCost = typeof log.totalCost === 'number'
? log.totalCost.toFixed(2)
: '0.00';
// Safe fuel units and cost per unit
const fuelUnits = typeof log.fuelUnits === 'number'
? log.fuelUnits.toFixed(3)
: '0.000';
const costPerUnit = typeof log.costPerUnit === 'number'
? log.costPerUnit.toFixed(3)
: '0.000';
// Safe distance display
const distanceText = log.odometerReading
? `Odo: ${log.odometerReading}`
: log.tripDistance
? `Trip: ${log.tripDistance}`
: 'No distance';
return (
<ListItem
key={log.id}
divider
sx={{
flexDirection: isMobile ? 'column' : 'row',
alignItems: isMobile ? 'stretch' : 'center',
gap: isMobile ? 1 : 0,
py: isMobile ? 2 : 1
}}
>
<Box sx={{
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
alignItems: isMobile ? 'flex-start' : 'center',
flex: 1,
gap: isMobile ? 0.5 : 1
}}>
<ListItemText
primary={`${dateText} $${totalCost}`}
secondary={`${fuelUnits} @ $${costPerUnit}${distanceText}`}
sx={{ flex: 1, minWidth: 0 }}
/>
{log.efficiency &&
typeof log.efficiency === 'number' &&
!isNaN(log.efficiency) &&
log.efficiencyLabel && (
<Box sx={{ mr: isMobile ? 0 : 1 }}>
<Chip
label={`${log.efficiency.toFixed(1)} ${log.efficiencyLabel}`}
size="small"
color="primary"
/>
</Box>
)}
</Box>
<Box sx={{
display: 'flex',
gap: isMobile ? 1 : 0.5,
justifyContent: isMobile ? 'center' : 'flex-end',
width: isMobile ? '100%' : 'auto'
}}>
{onEdit && (
<IconButton
size={isMobile ? 'medium' : 'small'}
onClick={() => onEdit(log)}
sx={{
color: 'primary.main',
'&:hover': { backgroundColor: 'primary.main', color: 'white' },
minWidth: isMobile ? 48 : 'auto',
minHeight: isMobile ? 48 : 'auto',
...(isMobile && {
border: '1px solid',
borderColor: 'primary.main',
borderRadius: 2
})
}}
>
<Edit fontSize={isMobile ? 'medium' : 'small'} />
</IconButton>
)}
{onDelete && (
<IconButton
size={isMobile ? 'medium' : 'small'}
onClick={() => handleDeleteClick(log)}
sx={{
color: 'error.main',
'&:hover': { backgroundColor: 'error.main', color: 'white' },
minWidth: isMobile ? 48 : 'auto',
minHeight: isMobile ? 48 : 'auto',
...(isMobile && {
border: '1px solid',
borderColor: 'error.main',
borderRadius: 2
})
}}
>
<Delete fontSize={isMobile ? 'medium' : 'small'} />
</IconButton>
)}
</Box>
</ListItem>
);
} catch (error) {
console.error('[FuelLogsList] Error rendering log:', log, error);
return (
<ListItem key={log.id || Math.random()} divider>
<ListItemText
primary="Error displaying fuel log"
secondary="Data formatting issue"
/>
</ListItem>
);
}
}).filter(Boolean)}
</List>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onClose={handleDeleteCancel}>
<DialogTitle>Delete Fuel Log</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete this fuel log entry? This action cannot be undone.
</Typography>
{logToDelete && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{new Date(logToDelete.dateTime).toLocaleString()} - ${logToDelete.totalCost.toFixed(2)}
</Typography>
)}
</ListItem>
))}
</List>
</DialogContent>
<DialogActions>
<Button onClick={handleDeleteCancel} disabled={isDeleting}>
Cancel
</Button>
<Button
onClick={handleDeleteConfirm}
color="error"
variant="contained"
disabled={isDeleting}
>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
</DialogActions>
</Dialog>
</>
);
};

View File

@@ -7,8 +7,17 @@ export const FuelStatsCard: React.FC<{ logs?: FuelLogResponse[] }> = ({ logs })
const { unitSystem } = useUnits();
const stats = useMemo(() => {
if (!logs || logs.length === 0) return { count: 0, totalUnits: 0, totalCost: 0 };
const totalUnits = logs.reduce((s, l) => s + (l.fuelUnits || 0), 0);
const totalCost = logs.reduce((s, l) => s + (l.totalCost || 0), 0);
const totalUnits = logs.reduce((s, l) => {
const fuelUnits = typeof l.fuelUnits === 'number' && !isNaN(l.fuelUnits) ? l.fuelUnits : 0;
return s + fuelUnits;
}, 0);
const totalCost = logs.reduce((s, l) => {
const cost = typeof l.totalCost === 'number' && !isNaN(l.totalCost) ? l.totalCost : 0;
return s + cost;
}, 0);
return { count: logs.length, totalUnits, totalCost };
}, [logs]);
@@ -24,11 +33,11 @@ export const FuelStatsCard: React.FC<{ logs?: FuelLogResponse[] }> = ({ logs })
</Grid>
<Grid item xs={4}>
<Typography variant="overline" color="text.secondary">Total Fuel</Typography>
<Typography variant="h6">{(stats.totalUnits || 0).toFixed(2)} {unitLabel}</Typography>
<Typography variant="h6">{(typeof stats.totalUnits === 'number' && !isNaN(stats.totalUnits) ? stats.totalUnits : 0).toFixed(2)} {unitLabel}</Typography>
</Grid>
<Grid item xs={4}>
<Typography variant="overline" color="text.secondary">Total Cost</Typography>
<Typography variant="h6">${(stats.totalCost || 0).toFixed(2)}</Typography>
<Typography variant="h6">${(typeof stats.totalCost === 'number' && !isNaN(stats.totalCost) ? stats.totalCost : 0).toFixed(2)}</Typography>
</Grid>
</Grid>
</CardContent>

View File

@@ -1,24 +1,102 @@
import React from 'react';
import { Grid, Typography } from '@mui/material';
import React, { useState } from 'react';
import { Grid, Typography, Box } from '@mui/material';
import { useQueryClient } from '@tanstack/react-query';
import { FuelLogForm } from '../components/FuelLogForm';
import { FuelLogsList } from '../components/FuelLogsList';
import { FuelLogEditDialog } from '../components/FuelLogEditDialog';
import { useFuelLogs } from '../hooks/useFuelLogs';
import { FuelStatsCard } from '../components/FuelStatsCard';
import { FormSuspense } from '../../../components/SuspenseWrappers';
import { FuelLogResponse, UpdateFuelLogRequest } from '../types/fuel-logs.types';
import { fuelLogsApi } from '../api/fuel-logs.api';
export const FuelLogsPage: React.FC = () => {
const { fuelLogs } = useFuelLogs();
const { fuelLogs, isLoading, error } = useFuelLogs();
const queryClient = useQueryClient();
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
const handleEdit = (log: FuelLogResponse) => {
setEditingLog(log);
};
const handleDelete = async (_logId: string) => {
try {
// Invalidate queries to refresh the data
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
} catch (error) {
console.error('Failed to refresh fuel logs after delete:', error);
}
};
const handleSaveEdit = async (id: string, data: UpdateFuelLogRequest) => {
try {
await fuelLogsApi.update(id, data);
// Invalidate queries to refresh the data
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
setEditingLog(null);
} catch (error) {
console.error('Failed to update fuel log:', error);
throw error; // Re-throw to let the dialog handle the error
}
};
const handleCloseEdit = () => {
setEditingLog(null);
};
if (isLoading) {
return (
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '50vh'
}}>
<Typography color="text.secondary">Loading fuel logs...</Typography>
</Box>
);
}
if (error) {
return (
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '50vh'
}}>
<Typography color="error">Failed to load fuel logs. Please try again.</Typography>
</Box>
);
}
return (
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<FuelLogForm />
<FormSuspense>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<FuelLogForm />
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="h6" gutterBottom>Recent Fuel Logs</Typography>
<FuelLogsList
logs={fuelLogs}
onEdit={handleEdit}
onDelete={handleDelete}
/>
<Typography variant="h6" sx={{ mt: 3 }} gutterBottom>Summary</Typography>
<FuelStatsCard logs={fuelLogs} />
</Grid>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="h6" gutterBottom>Recent Fuel Logs</Typography>
<FuelLogsList logs={fuelLogs} />
<Typography variant="h6" sx={{ mt: 3 }} gutterBottom>Summary</Typography>
<FuelStatsCard logs={fuelLogs} />
</Grid>
</Grid>
{/* Edit Dialog */}
<FuelLogEditDialog
open={!!editingLog}
log={editingLog}
onClose={handleCloseEdit}
onSave={handleSaveEdit}
/>
</FormSuspense>
);
};

View File

@@ -34,6 +34,18 @@ export interface CreateFuelLogRequest {
notes?: string;
}
export interface UpdateFuelLogRequest {
dateTime?: string;
odometerReading?: number;
tripDistance?: number;
fuelType?: FuelType;
fuelGrade?: FuelGrade;
fuelUnits?: number;
costPerUnit?: number;
locationData?: LocationData;
notes?: string;
}
export interface FuelLogResponse {
id: string;
userId: string;