Compare commits

..

6 Commits

Author SHA1 Message Date
e729d425fd Merge pull request 'feat: Reorder Log Fuel fields by usage and add decimal keypad (#246)' (#247) from issue-246-reorder-log-fuel-fields into main
All checks were successful
Deploy to Staging / Build Images (push) Successful in 1m2s
Deploy to Staging / Deploy to Staging (push) Successful in 42s
Deploy to Staging / Verify Staging (push) Successful in 4s
Deploy to Staging / Notify Staging Ready (push) Successful in 3s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Mirror Base Images / Mirror Base Images (push) Successful in 28s
Reviewed-on: #247
2026-06-20 03:44:26 +00:00
Eric Gullickson
2221819b4a feat: reorder Log Fuel fields by usage and add decimal keypad (refs #246)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m10s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 44s
Deploy to Staging / Verify Staging (pull_request) Successful in 4s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 4s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Reorder the Add Fuel Log form so the most-used inputs come first and
calculated/auto-filled fields are de-emphasized:
Vehicle, Mileage, Cost Per Gallon, Fuel Amount, Fuel Type/Grade,
Location, MPG (read-only), Notes, Date & Time.

Mileage is the existing Trip Distance / Odometer Reading field,
repositioned only; toggle and logic unchanged. Fuel Type stays before
Fuel Grade to preserve the type-filters-grade dependency.

Add inputMode=decimal to the numeric fields (Mileage via DistanceInput,
Fuel Amount, Cost Per Gallon) so mobile shows the decimal keypad. MPG
and Date & Time become full-width since they are no longer paired.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 22:18:46 -05:00
77aeba0102 Merge pull request 'fix: coerce decimals in fuel-logs enhanced repository methods (#244)' (#245) from issue-244-fuel-logs-enhanced-mapper into main
All checks were successful
Deploy to Staging / Build Images (push) Successful in 10s
Deploy to Staging / Deploy to Staging (push) Successful in 14s
Deploy to Staging / Verify Staging (push) Successful in 3s
Deploy to Staging / Notify Staging Ready (push) Successful in 3s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Mirror Base Images / Mirror Base Images (push) Successful in 26s
Reviewed-on: #245
2026-05-16 02:58:52 +00:00
Eric Gullickson
0d90829d31 fix: coerce decimals in fuel-logs enhanced repository methods (refs #244)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 35s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 43s
Deploy to Staging / Verify Staging (pull_request) Successful in 3s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 3s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
The enhanced API path on FuelLogsRepository returned raw pg rows
straight to the service layer, so DECIMAL columns (fuel_units,
cost_per_unit, total_cost, gallons, price_per_gallon) arrived as
strings instead of numbers. The service layer compensated with
scattered Number() coercion in toEnhancedResponse and getVehicleStats,
and EfficiencyCalculationService silently leaned on JS string-to-
number coercion in division. Type signatures across the stack
declared number and the runtime delivered string.

Add a private mapEnhancedRow that coerces all DECIMAL columns with
parseFloat while preserving snake_case keys (the convention the
service layer already uses to access these rows). Apply it in every
enhanced read/write path: createEnhanced, findByVehicleIdEnhanced,
findByUserIdEnhanced, findByIdEnhanced, getPreviousLogByOdometer,
getLatestLogForVehicle, updateEnhanced. Drop the now-redundant
Number() wrappers in toEnhancedResponse and getVehicleStats.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:46:48 -05:00
0dd5746f60 Merge pull request 'fix: coerce numeric/decimal columns in repository mappers (#241)' (#242) from issue-241-numeric-mapper-coercion into main
All checks were successful
Deploy to Staging / Build Images (push) Successful in 10s
Deploy to Staging / Deploy to Staging (push) Successful in 41s
Deploy to Staging / Verify Staging (push) Successful in 3s
Deploy to Staging / Notify Staging Ready (push) Successful in 4s
Deploy to Staging / Notify Staging Failure (push) Has been skipped
Reviewed-on: #242
2026-05-16 02:35:02 +00:00
Eric Gullickson
fdc34aee2f fix: coerce numeric/decimal columns in repository mappers (refs #241)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 1m49s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 43s
Deploy to Staging / Verify Staging (pull_request) Successful in 4s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 3s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
node-postgres returns numeric/decimal columns as JavaScript strings,
but the TypeScript interfaces for MaintenanceRecord and OwnershipCost
declare numeric fields as number. The mappers were passing values
through raw, breaking type-safe arithmetic and display (e.g. the
amount column on the vehicle summary screen was empty until the
recent frontend workaround in PR #240, and OwnershipCostsList silently
no-ops toLocaleString on the string).

Backend
- mapMaintenanceRecord: coerce cost via Number() when non-null.
- ownership-costs mapRow: coerce amount via Number().

Frontend (remove now-redundant workarounds)
- MaintenanceRecordsList: drop Number() coercion on cost and
  odometerReading; use the number values directly.
- VehicleDetailPage / VehicleDetailMobile: revert the PR #240 cost
  coercion to the simple typeof number guard now that the backend
  honors the type.

Scope notes
- Other repositories with the same pattern (stations, community-stations,
  fuel-logs enhanced methods) are tracked separately because they have
  unclear downstream consumers and warrant their own investigation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 21:06:24 -05:00
9 changed files with 122 additions and 100 deletions

View File

@@ -247,6 +247,20 @@ export class FuelLogsRepository {
}; };
} }
// Coerces DECIMAL columns from node-postgres strings to numbers while
// preserving snake_case row keys used by the enhanced API service callers.
private mapEnhancedRow(row: any): any {
if (!row) return null;
return {
...row,
fuel_units: row.fuel_units != null ? parseFloat(row.fuel_units) : row.fuel_units,
cost_per_unit: row.cost_per_unit != null ? parseFloat(row.cost_per_unit) : row.cost_per_unit,
total_cost: row.total_cost != null ? parseFloat(row.total_cost) : row.total_cost,
gallons: row.gallons != null ? parseFloat(row.gallons) : row.gallons,
price_per_gallon: row.price_per_gallon != null ? parseFloat(row.price_per_gallon) : row.price_per_gallon,
};
}
// Enhanced API support (new schema) // Enhanced API support (new schema)
async createEnhanced(data: { async createEnhanced(data: {
userId: string; userId: string;
@@ -293,7 +307,7 @@ export class FuelLogsRepository {
data.notes ?? null data.notes ?? null
]; ];
const res = await this.pool.query(query, values); const res = await this.pool.query(query, values);
return res.rows[0] ?? null; return this.mapEnhancedRow(res.rows[0] ?? null);
} }
async findByVehicleIdEnhanced(vehicleId: string): Promise<any[]> { async findByVehicleIdEnhanced(vehicleId: string): Promise<any[]> {
@@ -301,7 +315,7 @@ export class FuelLogsRepository {
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`, `SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`,
[vehicleId] [vehicleId]
); );
return res.rows; return res.rows.map(r => this.mapEnhancedRow(r));
} }
async findByUserIdEnhanced(userId: string): Promise<any[]> { async findByUserIdEnhanced(userId: string): Promise<any[]> {
@@ -309,12 +323,12 @@ export class FuelLogsRepository {
`SELECT * FROM fuel_logs WHERE user_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`, `SELECT * FROM fuel_logs WHERE user_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC`,
[userId] [userId]
); );
return res.rows; return res.rows.map(r => this.mapEnhancedRow(r));
} }
async findByIdEnhanced(id: string): Promise<any | null> { async findByIdEnhanced(id: string): Promise<any | null> {
const res = await this.pool.query(`SELECT * FROM fuel_logs WHERE id = $1`, [id]); const res = await this.pool.query(`SELECT * FROM fuel_logs WHERE id = $1`, [id]);
return res.rows[0] ?? null; return this.mapEnhancedRow(res.rows[0] ?? null);
} }
async getPreviousLogByOdometer(vehicleId: string, odometerReading: number): Promise<any | null> { async getPreviousLogByOdometer(vehicleId: string, odometerReading: number): Promise<any | null> {
@@ -322,7 +336,7 @@ export class FuelLogsRepository {
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 AND odometer IS NOT NULL AND odometer < $2 ORDER BY odometer DESC LIMIT 1`, `SELECT * FROM fuel_logs WHERE vehicle_id = $1 AND odometer IS NOT NULL AND odometer < $2 ORDER BY odometer DESC LIMIT 1`,
[vehicleId, odometerReading] [vehicleId, odometerReading]
); );
return res.rows[0] ?? null; return this.mapEnhancedRow(res.rows[0] ?? null);
} }
async getLatestLogForVehicle(vehicleId: string): Promise<any | null> { async getLatestLogForVehicle(vehicleId: string): Promise<any | null> {
@@ -330,7 +344,7 @@ export class FuelLogsRepository {
`SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC LIMIT 1`, `SELECT * FROM fuel_logs WHERE vehicle_id = $1 ORDER BY date_time DESC NULLS LAST, date DESC NULLS LAST, created_at DESC LIMIT 1`,
[vehicleId] [vehicleId]
); );
return res.rows[0] ?? null; return this.mapEnhancedRow(res.rows[0] ?? null);
} }
async updateEnhanced(id: string, data: { async updateEnhanced(id: string, data: {
@@ -416,6 +430,6 @@ export class FuelLogsRepository {
return null; return null;
} }
return result.rows[0]; return this.mapEnhancedRow(result.rows[0]);
} }
} }

View File

@@ -234,8 +234,8 @@ export class FuelLogsService {
return { logCount: 0, totalFuelUnits: 0, totalCost: 0, averageCostPerUnit: 0, totalDistance: 0, averageEfficiency: 0, unitLabels: labels }; return { logCount: 0, totalFuelUnits: 0, totalCost: 0, averageCostPerUnit: 0, totalDistance: 0, averageEfficiency: 0, unitLabels: labels };
} }
const totalFuelUnits = rows.reduce((s, r) => s + (Number(r.fuel_units) || 0), 0); const totalFuelUnits = rows.reduce((s, r) => s + (r.fuel_units || 0), 0);
const totalCost = rows.reduce((s, r) => s + (Number(r.total_cost) || 0), 0); const totalCost = rows.reduce((s, r) => s + (r.total_cost || 0), 0);
const averageCostPerUnit = totalFuelUnits > 0 ? totalCost / totalFuelUnits : 0; const averageCostPerUnit = totalFuelUnits > 0 ? totalCost / totalFuelUnits : 0;
const sorted = [...rows].sort((a, b) => (new Date(b.date_time || b.date)).getTime() - (new Date(a.date_time || a.date)).getTime()); const sorted = [...rows].sort((a, b) => (new Date(b.date_time || b.date)).getTime() - (new Date(a.date_time || a.date)).getTime());
@@ -282,9 +282,9 @@ export class FuelLogsService {
tripDistance: row.trip_distance ?? undefined, tripDistance: row.trip_distance ?? undefined,
fuelType: row.fuel_type as FuelType, fuelType: row.fuel_type as FuelType,
fuelGrade: row.fuel_grade ?? undefined, fuelGrade: row.fuel_grade ?? undefined,
fuelUnits: Number(row.fuel_units), fuelUnits: row.fuel_units ?? 0,
costPerUnit: Number(row.cost_per_unit), costPerUnit: row.cost_per_unit ?? 0,
totalCost: Number(row.total_cost), totalCost: row.total_cost ?? 0,
locationData: row.location_data ?? undefined, locationData: row.location_data ?? undefined,
notes: row.notes ?? undefined, notes: row.notes ?? undefined,
efficiency: efficiency, efficiency: efficiency,

View File

@@ -18,7 +18,8 @@ export class MaintenanceRepository {
subtypes: row.subtypes, subtypes: row.subtypes,
date: row.date, date: row.date,
odometerReading: row.odometer_reading, odometerReading: row.odometer_reading,
cost: row.cost, // node-postgres returns numeric/decimal columns as strings; coerce to honor the number type.
cost: row.cost != null ? Number(row.cost) : undefined,
shopName: row.shop_name, shopName: row.shop_name,
notes: row.notes, notes: row.notes,
receiptDocumentId: row.receipt_document_id, receiptDocumentId: row.receipt_document_id,

View File

@@ -16,7 +16,8 @@ export class OwnershipCostsRepository {
vehicleId: row.vehicle_id, vehicleId: row.vehicle_id,
documentId: row.document_id, documentId: row.document_id,
costType: row.cost_type, costType: row.cost_type,
amount: row.amount, // node-postgres returns numeric/decimal columns as strings; coerce to honor the number type.
amount: Number(row.amount),
description: row.description, description: row.description,
periodStart: row.period_start, periodStart: row.period_start,
periodEnd: row.period_end, periodEnd: row.period_end,

View File

@@ -24,7 +24,7 @@ export const DistanceInput: React.FC<Props> = ({ type, value, onChange, unitSyst
fullWidth fullWidth
error={!!error} error={!!error}
disabled={disabled} disabled={disabled}
inputProps={{ step: type === 'trip' ? 0.1 : 1, min: 0 }} inputProps={{ step: type === 'trip' ? 0.1 : 1, min: 0, inputMode: 'decimal' }}
InputProps={{ endAdornment: <InputAdornment position="end">{units}</InputAdornment> }} InputProps={{ endAdornment: <InputAdornment position="end">{units}</InputAdornment> }}
/> />
{error && <FormHelperText error>{error}</FormHelperText>} {error && <FormHelperText error>{error}</FormHelperText>}

View File

@@ -247,60 +247,7 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
)} /> )} />
</Grid> </Grid>
{/* Row 2: Date/Time | MPG/L/100km */} {/* Row 2: Mileage (Trip Distance / Odometer) | Input Method Toggle */}
<Grid item xs={12} sm={6}>
<Controller name="dateTime" control={control} render={({ field }) => (
<DateTimePicker
label="Date & Time"
value={field.value ? dayjs(field.value) : null}
onChange={(newValue) => field.onChange(newValue?.toISOString() || '')}
format="MM/DD/YYYY hh:mm a"
slotProps={{
textField: {
fullWidth: true,
error: !!errors.dateTime,
helperText: errors.dateTime?.message,
sx: {
'& .MuiOutlinedInput-root': {
minHeight: '56px',
}
}
}
}}
/>
)} />
</Grid>
<Grid item xs={12} sm={6}>
<TextField
label={`${userSettings?.unitSystem === 'metric' ? 'L/100km' : 'MPG'}`}
value={calculatedEfficiency > 0 ? calculatedEfficiency.toFixed(3) : ''}
fullWidth
InputProps={{
readOnly: true,
sx: (theme) => ({
backgroundColor: 'grey.50',
...theme.applyStyles('dark', {
backgroundColor: '#4C4E4D',
}),
'& .MuiOutlinedInput-input': {
cursor: 'default',
color: 'inherit',
...theme.applyStyles('dark', {
color: '#F2F3F6',
}),
},
}),
}}
helperText="Calculated from distance ÷ fuel amount"
sx={{
'& .MuiOutlinedInput-root': {
minHeight: '56px',
}
}}
/>
</Grid>
{/* Row 3: Odometer | Distance Input Method */}
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<Controller name={useOdometer ? 'odometerReading' : 'tripDistance'} control={control} render={({ field }) => ( <Controller name={useOdometer ? 'odometerReading' : 'tripDistance'} control={control} render={({ field }) => (
<DistanceInput type={useOdometer ? 'odometer' : 'trip'} value={field.value as any} onChange={field.onChange as any} unitSystem={userSettings?.unitSystem} error={useOdometer ? (errors.odometerReading?.message as any) : (errors.tripDistance?.message as any)} /> <DistanceInput type={useOdometer ? 'odometer' : 'trip'} value={field.value as any} onChange={field.onChange as any} unitSystem={userSettings?.unitSystem} error={useOdometer ? (errors.odometerReading?.message as any) : (errors.tripDistance?.message as any)} />
@@ -342,11 +289,21 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</Grid> </Grid>
<Grid item xs={12}>
<Controller name="fuelType" control={control} render={({ field: fuelTypeField }) => ( {/* Row 3: Cost Per Gallon | Fuel Amount */}
<Controller name="fuelGrade" control={control} render={({ field: fuelGradeField }) => ( <Grid item xs={12} sm={6}>
<FuelTypeSelector fuelType={fuelTypeField.value} fuelGrade={fuelGradeField.value as any} onFuelTypeChange={fuelTypeField.onChange} onFuelGradeChange={fuelGradeField.onChange as any} error={(errors.fuelType?.message as any) || (errors.fuelGrade?.message as any)} /> <Controller name="costPerUnit" control={control} render={({ field }) => (
)} /> <TextField
{...field}
value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.value)}
label={`Cost Per ${userSettings?.unitSystem === 'imperial' ? 'Gallon' : 'Liter'}`}
type="number"
inputProps={{ step: 0.001, min: 0.001, inputMode: 'decimal' }}
fullWidth
error={!!errors.costPerUnit}
helperText={errors.costPerUnit?.message}
/>
)} /> )} />
</Grid> </Grid>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
@@ -357,31 +314,27 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
onChange={(e) => field.onChange(e.target.value)} onChange={(e) => field.onChange(e.target.value)}
label={`Fuel Amount (${userSettings?.unitSystem === 'imperial' ? 'gallons' : 'liters'})`} label={`Fuel Amount (${userSettings?.unitSystem === 'imperial' ? 'gallons' : 'liters'})`}
type="number" type="number"
inputProps={{ step: 0.001, min: 0.001 }} inputProps={{ step: 0.001, min: 0.001, inputMode: 'decimal' }}
fullWidth fullWidth
error={!!errors.fuelUnits} error={!!errors.fuelUnits}
helperText={errors.fuelUnits?.message} helperText={errors.fuelUnits?.message}
/> />
)} /> )} />
</Grid> </Grid>
<Grid item xs={12} sm={6}>
<Controller name="costPerUnit" control={control} render={({ field }) => (
<TextField
{...field}
value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.value)}
label={`Cost Per ${userSettings?.unitSystem === 'imperial' ? 'Gallon' : 'Liter'}`}
type="number"
inputProps={{ step: 0.001, min: 0.001 }}
fullWidth
error={!!errors.costPerUnit}
helperText={errors.costPerUnit?.message}
/>
)} />
</Grid>
<Grid item xs={12}> <Grid item xs={12}>
<CostCalculator fuelUnits={fuelUnits} costPerUnit={costPerUnit} calculatedCost={calculatedCost} /> <CostCalculator fuelUnits={fuelUnits} costPerUnit={costPerUnit} calculatedCost={calculatedCost} />
</Grid> </Grid>
{/* Row 4: Fuel Type / Fuel Grade (grade options filtered by type) */}
<Grid item xs={12}>
<Controller name="fuelType" control={control} render={({ field: fuelTypeField }) => (
<Controller name="fuelGrade" control={control} render={({ field: fuelGradeField }) => (
<FuelTypeSelector fuelType={fuelTypeField.value} fuelGrade={fuelGradeField.value as any} onFuelTypeChange={fuelTypeField.onChange} onFuelGradeChange={fuelGradeField.onChange as any} error={(errors.fuelType?.message as any) || (errors.fuelGrade?.message as any)} />
)} />
)} />
</Grid>
{/* Row 5: Location */}
<Grid item xs={12}> <Grid item xs={12}>
<Controller name="locationData" control={control} render={({ field }) => ( <Controller name="locationData" control={control} render={({ field }) => (
<StationPicker <StationPicker
@@ -392,11 +345,68 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
/> />
)} /> )} />
</Grid> </Grid>
{/* Row 6: MPG/L/100km (calculated, read-only) */}
<Grid item xs={12}>
<TextField
label={`${userSettings?.unitSystem === 'metric' ? 'L/100km' : 'MPG'}`}
value={calculatedEfficiency > 0 ? calculatedEfficiency.toFixed(3) : ''}
fullWidth
InputProps={{
readOnly: true,
sx: (theme) => ({
backgroundColor: 'grey.50',
...theme.applyStyles('dark', {
backgroundColor: '#4C4E4D',
}),
'& .MuiOutlinedInput-input': {
cursor: 'default',
color: 'inherit',
...theme.applyStyles('dark', {
color: '#F2F3F6',
}),
},
}),
}}
helperText="Calculated from distance ÷ fuel amount"
sx={{
'& .MuiOutlinedInput-root': {
minHeight: '56px',
}
}}
/>
</Grid>
{/* Row 7: Notes */}
<Grid item xs={12}> <Grid item xs={12}>
<Controller name="notes" control={control} render={({ field }) => ( <Controller name="notes" control={control} render={({ field }) => (
<TextField {...field} label="Notes (optional)" multiline rows={3} fullWidth error={!!errors.notes} helperText={errors.notes?.message} /> <TextField {...field} label="Notes (optional)" multiline rows={3} fullWidth error={!!errors.notes} helperText={errors.notes?.message} />
)} /> )} />
</Grid> </Grid>
{/* Row 8: Date & Time (defaults to now, least-edited field) */}
<Grid item xs={12}>
<Controller name="dateTime" control={control} render={({ field }) => (
<DateTimePicker
label="Date & Time"
value={field.value ? dayjs(field.value) : null}
onChange={(newValue) => field.onChange(newValue?.toISOString() || '')}
format="MM/DD/YYYY hh:mm a"
slotProps={{
textField: {
fullWidth: true,
error: !!errors.dateTime,
helperText: errors.dateTime?.message,
sx: {
'& .MuiOutlinedInput-root': {
minHeight: '56px',
}
}
}
}}
/>
)} />
</Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Box display="flex" gap={2} justifyContent="flex-end"> <Box display="flex" gap={2} justifyContent="flex-end">
<Button type="submit" variant="contained" disabled={!isValid || isLoading} startIcon={isLoading ? <CircularProgress size={18} /> : undefined}>Add Fuel Log</Button> <Button type="submit" variant="contained" disabled={!isValid || isLoading} startIcon={isLoading ? <CircularProgress size={18} /> : undefined}>Add Fuel Log</Button>

View File

@@ -115,16 +115,16 @@ export const MaintenanceRecordsList: React.FC<MaintenanceRecordsListProps> = ({
{categoryDisplay} ({subtypeCount}) {categoryDisplay} ({subtypeCount})
</Typography> </Typography>
<Stack direction="row" spacing={1} flexWrap="wrap" sx={{ mt: 1 }}> <Stack direction="row" spacing={1} flexWrap="wrap" sx={{ mt: 1 }}>
{record.odometerReading && ( {record.odometerReading != null && (
<Chip <Chip
label={`${Number(record.odometerReading).toLocaleString()} miles`} label={`${record.odometerReading.toLocaleString()} miles`}
size="small" size="small"
variant="outlined" variant="outlined"
/> />
)} )}
{record.cost && ( {record.cost != null && (
<Chip <Chip
label={`$${Number(record.cost).toFixed(2)}`} label={`$${record.cost.toFixed(2)}`}
size="small" size="small"
color="primary" color="primary"
variant="outlined" variant="outlined"

View File

@@ -142,9 +142,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
secondaryParts.push(new Date(rec.date).toLocaleDateString()); secondaryParts.push(new Date(rec.date).toLocaleDateString());
secondaryParts.push('Maintenance'); secondaryParts.push('Maintenance');
const secondary = secondaryParts.join(' • '); const secondary = secondaryParts.join(' • ');
// Backend returns numeric/decimal columns as strings via node-postgres; coerce. const amount = typeof rec.cost === 'number' ? `$${rec.cost.toFixed(2)}` : undefined;
const costNum = rec.cost != null ? Number(rec.cost) : NaN;
const amount = Number.isFinite(costNum) ? `$${costNum.toFixed(2)}` : undefined;
list.push({ list.push({
id: rec.id, id: rec.id,
type: 'Maintenance', type: 'Maintenance',

View File

@@ -148,9 +148,7 @@ export const VehicleDetailPage: React.FC = () => {
if (subtypeText) parts.push(subtypeText); if (subtypeText) parts.push(subtypeText);
if (rec.shopName) parts.push(rec.shopName); if (rec.shopName) parts.push(rec.shopName);
const summary = parts.join(' • '); const summary = parts.join(' • ');
// Backend returns numeric/decimal columns as strings via node-postgres; coerce. const amount = typeof rec.cost === 'number' ? `$${rec.cost.toFixed(2)}` : undefined;
const costNum = rec.cost != null ? Number(rec.cost) : NaN;
const amount = Number.isFinite(costNum) ? `$${costNum.toFixed(2)}` : undefined;
list.push({ id: rec.id, type: 'Maintenance', date: rec.date, summary, amount }); list.push({ id: rec.id, type: 'Maintenance', date: rec.date, summary, amount });
} }
} }