Compare commits
6 Commits
068bb751a7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ac9c13c9d3 | |||
|
|
1e056f0b01 | ||
|
|
087f7b9fa5 | ||
|
|
f0fc427ccd | ||
|
|
c05e33e230 | ||
|
|
8955baae26 |
@@ -2,10 +2,15 @@
|
|||||||
* @ai-summary PostgreSQL connection pool configuration
|
* @ai-summary PostgreSQL connection pool configuration
|
||||||
* @ai-context Shared pool for all feature repositories
|
* @ai-context Shared pool for all feature repositories
|
||||||
*/
|
*/
|
||||||
import { Pool } from 'pg';
|
import { Pool, types } from 'pg';
|
||||||
import { logger } from '../logging/logger';
|
import { logger } from '../logging/logger';
|
||||||
import { appConfig } from './config-loader';
|
import { appConfig } from './config-loader';
|
||||||
|
|
||||||
|
// Override DATE type parser to return plain YYYY-MM-DD strings instead of Date objects.
|
||||||
|
// Default pg behavior creates Date objects at local midnight, which shift dates when
|
||||||
|
// serialized to JSON via toISOString() (UTC conversion) for clients in other timezones.
|
||||||
|
types.setTypeParser(1082, (val: string) => val);
|
||||||
|
|
||||||
export const pool = new Pool({
|
export const pool = new Pool({
|
||||||
connectionString: appConfig.getDatabaseUrl(),
|
connectionString: appConfig.getDatabaseUrl(),
|
||||||
max: 10,
|
max: 10,
|
||||||
|
|||||||
@@ -335,9 +335,9 @@ export const MaintenanceRecordEditDialog: React.FC<MaintenanceRecordEditDialogPr
|
|||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Service Date *"
|
label="Service Date *"
|
||||||
value={formData.date ? dayjs(formData.date) : null}
|
value={formData.date ? dayjs(String(formData.date).substring(0, 10)) : null}
|
||||||
onChange={(newValue) =>
|
onChange={(newValue) =>
|
||||||
handleInputChange('date', newValue?.toISOString().split('T')[0] || '')
|
handleInputChange('date', newValue?.format('YYYY-MM-DD') || '')
|
||||||
}
|
}
|
||||||
format="MM/DD/YYYY"
|
format="MM/DD/YYYY"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ export const MaintenanceRecordForm: React.FC<MaintenanceRecordFormProps> = ({ ve
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
vehicle_id: vehicleId || '',
|
vehicle_id: vehicleId || '',
|
||||||
category: undefined as any,
|
category: '' as any,
|
||||||
subtypes: [],
|
subtypes: [],
|
||||||
date: new Date().toISOString().split('T')[0],
|
date: dayjs().format('YYYY-MM-DD'),
|
||||||
odometer_reading: '' as any,
|
odometer_reading: '' as any,
|
||||||
cost: '' as any,
|
cost: '' as any,
|
||||||
shop_name: '',
|
shop_name: '',
|
||||||
@@ -132,6 +132,8 @@ export const MaintenanceRecordForm: React.FC<MaintenanceRecordFormProps> = ({ ve
|
|||||||
if (watchedCategory) {
|
if (watchedCategory) {
|
||||||
setSelectedCategory(watchedCategory as MaintenanceCategory);
|
setSelectedCategory(watchedCategory as MaintenanceCategory);
|
||||||
setValue('subtypes', []);
|
setValue('subtypes', []);
|
||||||
|
} else {
|
||||||
|
setSelectedCategory(null);
|
||||||
}
|
}
|
||||||
}, [watchedCategory, setValue]);
|
}, [watchedCategory, setValue]);
|
||||||
|
|
||||||
@@ -188,7 +190,7 @@ export const MaintenanceRecordForm: React.FC<MaintenanceRecordFormProps> = ({ ve
|
|||||||
const doc = await documentsApi.create({
|
const doc = await documentsApi.create({
|
||||||
vehicleId: data.vehicle_id,
|
vehicleId: data.vehicle_id,
|
||||||
documentType: 'manual',
|
documentType: 'manual',
|
||||||
title: `Maintenance Receipt - ${new Date(data.date).toLocaleDateString()}`,
|
title: `Maintenance Receipt - ${dayjs(data.date).format('M/D/YYYY')}`,
|
||||||
});
|
});
|
||||||
await documentsApi.upload(doc.id, capturedReceiptFile);
|
await documentsApi.upload(doc.id, capturedReceiptFile);
|
||||||
receiptDocumentId = doc.id;
|
receiptDocumentId = doc.id;
|
||||||
@@ -217,9 +219,9 @@ export const MaintenanceRecordForm: React.FC<MaintenanceRecordFormProps> = ({ ve
|
|||||||
// Reset form
|
// Reset form
|
||||||
reset({
|
reset({
|
||||||
vehicle_id: '',
|
vehicle_id: '',
|
||||||
category: undefined as any,
|
category: '' as any,
|
||||||
subtypes: [],
|
subtypes: [],
|
||||||
date: new Date().toISOString().split('T')[0],
|
date: dayjs().format('YYYY-MM-DD'),
|
||||||
odometer_reading: '' as any,
|
odometer_reading: '' as any,
|
||||||
cost: '' as any,
|
cost: '' as any,
|
||||||
shop_name: '',
|
shop_name: '',
|
||||||
@@ -385,7 +387,7 @@ export const MaintenanceRecordForm: React.FC<MaintenanceRecordFormProps> = ({ ve
|
|||||||
label="Date *"
|
label="Date *"
|
||||||
value={field.value ? dayjs(field.value) : null}
|
value={field.value ? dayjs(field.value) : null}
|
||||||
onChange={(newValue) =>
|
onChange={(newValue) =>
|
||||||
field.onChange(newValue?.toISOString().split('T')[0] || '')
|
field.onChange(newValue?.format('YYYY-MM-DD') || '')
|
||||||
}
|
}
|
||||||
format="MM/DD/YYYY"
|
format="MM/DD/YYYY"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -74,14 +75,14 @@ export const MaintenanceRecordsList: React.FC<MaintenanceRecordsListProps> = ({
|
|||||||
|
|
||||||
// Sort records by date DESC (newest first)
|
// Sort records by date DESC (newest first)
|
||||||
const sortedRecords = [...records].sort(
|
const sortedRecords = [...records].sort(
|
||||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
(a, b) => b.date.localeCompare(a.date)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
{sortedRecords.map((record) => {
|
{sortedRecords.map((record) => {
|
||||||
const dateText = new Date(record.date).toLocaleDateString();
|
const dateText = dayjs(record.date).format('M/D/YYYY');
|
||||||
const categoryDisplay = getCategoryDisplayName(record.category);
|
const categoryDisplay = getCategoryDisplayName(record.category);
|
||||||
const subtypeCount = record.subtypeCount || record.subtypes?.length || 0;
|
const subtypeCount = record.subtypeCount || record.subtypes?.length || 0;
|
||||||
|
|
||||||
@@ -204,7 +205,7 @@ export const MaintenanceRecordsList: React.FC<MaintenanceRecordsListProps> = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
{recordToDelete && (
|
{recordToDelete && (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
{new Date(recordToDelete.date).toLocaleDateString()} -{' '}
|
{dayjs(recordToDelete.date).format('M/D/YYYY')} -{' '}
|
||||||
{getCategoryDisplayName(recordToDelete.category)}
|
{getCategoryDisplayName(recordToDelete.category)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -335,9 +335,9 @@ export const MaintenanceScheduleEditDialog: React.FC<MaintenanceScheduleEditDial
|
|||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
label="Due Date"
|
label="Due Date"
|
||||||
value={formData.fixedDueDate ? dayjs(formData.fixedDueDate) : null}
|
value={formData.fixedDueDate ? dayjs(String(formData.fixedDueDate).substring(0, 10)) : null}
|
||||||
onChange={(newValue) =>
|
onChange={(newValue) =>
|
||||||
handleInputChange('fixedDueDate', newValue?.toISOString().split('T')[0] || undefined)
|
handleInputChange('fixedDueDate', newValue?.format('YYYY-MM-DD') || undefined)
|
||||||
}
|
}
|
||||||
format="MM/DD/YYYY"
|
format="MM/DD/YYYY"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const MaintenanceScheduleForm: React.FC<MaintenanceScheduleFormProps> = (
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
vehicle_id: vehicleId || '',
|
vehicle_id: vehicleId || '',
|
||||||
category: undefined as any,
|
category: '' as any,
|
||||||
subtypes: [],
|
subtypes: [],
|
||||||
schedule_type: 'interval' as ScheduleType,
|
schedule_type: 'interval' as ScheduleType,
|
||||||
interval_months: '' as any,
|
interval_months: '' as any,
|
||||||
@@ -145,6 +145,8 @@ export const MaintenanceScheduleForm: React.FC<MaintenanceScheduleFormProps> = (
|
|||||||
if (watchedCategory) {
|
if (watchedCategory) {
|
||||||
setSelectedCategory(watchedCategory as MaintenanceCategory);
|
setSelectedCategory(watchedCategory as MaintenanceCategory);
|
||||||
setValue('subtypes', []);
|
setValue('subtypes', []);
|
||||||
|
} else {
|
||||||
|
setSelectedCategory(null);
|
||||||
}
|
}
|
||||||
}, [watchedCategory, setValue]);
|
}, [watchedCategory, setValue]);
|
||||||
|
|
||||||
@@ -170,7 +172,7 @@ export const MaintenanceScheduleForm: React.FC<MaintenanceScheduleFormProps> = (
|
|||||||
// Reset form
|
// Reset form
|
||||||
reset({
|
reset({
|
||||||
vehicle_id: '',
|
vehicle_id: '',
|
||||||
category: undefined as any,
|
category: '' as any,
|
||||||
subtypes: [],
|
subtypes: [],
|
||||||
schedule_type: 'interval' as ScheduleType,
|
schedule_type: 'interval' as ScheduleType,
|
||||||
interval_months: '' as any,
|
interval_months: '' as any,
|
||||||
@@ -410,7 +412,7 @@ export const MaintenanceScheduleForm: React.FC<MaintenanceScheduleFormProps> = (
|
|||||||
label="Fixed Due Date *"
|
label="Fixed Due Date *"
|
||||||
value={field.value ? dayjs(field.value) : null}
|
value={field.value ? dayjs(field.value) : null}
|
||||||
onChange={(newValue) =>
|
onChange={(newValue) =>
|
||||||
field.onChange(newValue?.toISOString().split('T')[0] || '')
|
field.onChange(newValue?.format('YYYY-MM-DD') || '')
|
||||||
}
|
}
|
||||||
format="MM/DD/YYYY"
|
format="MM/DD/YYYY"
|
||||||
slotProps={{
|
slotProps={{
|
||||||
|
|||||||
@@ -107,17 +107,17 @@ function parseServiceDate(value: string | number | null): string | undefined {
|
|||||||
// Try standard parsing
|
// Try standard parsing
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
if (!isNaN(date.getTime())) {
|
if (!isNaN(date.getTime())) {
|
||||||
return date.toISOString().split('T')[0];
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try MM/DD/YYYY format
|
// Try MM/DD/YYYY format
|
||||||
const mdyMatch = dateStr.match(/(\d{1,2})\/(\d{1,2})\/(\d{4})/);
|
const mdyMatch = dateStr.match(/(\d{1,2})\/(\d{1,2})\/(\d{4})/);
|
||||||
if (mdyMatch) {
|
if (mdyMatch) {
|
||||||
const [, month, day, year] = mdyMatch;
|
const [, month, day, year] = mdyMatch;
|
||||||
const parsed = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
|
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
||||||
if (!isNaN(parsed.getTime())) {
|
|
||||||
return parsed.toISOString().split('T')[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import type { OwnershipCost, OwnershipCostType } from '../types/ownership-costs.types';
|
import type { OwnershipCost, OwnershipCostType } from '../types/ownership-costs.types';
|
||||||
import { useOwnershipCostsList, useDeleteOwnershipCost } from '../hooks/useOwnershipCosts';
|
import { useOwnershipCostsList, useDeleteOwnershipCost } from '../hooks/useOwnershipCosts';
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ export const OwnershipCostsList: React.FC<OwnershipCostsListProps> = ({ vehicleI
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string): string => {
|
const formatDate = (dateString: string): string => {
|
||||||
return new Date(dateString).toLocaleDateString();
|
return dayjs(dateString).format('M/D/YYYY');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { Box, Typography, Button, Card, CardContent, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem, ListItemButton, Dialog, DialogTitle, DialogContent } from '@mui/material';
|
import { Box, Typography, Button, Card, CardContent, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem, ListItemButton, Dialog, DialogTitle, DialogContent } from '@mui/material';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { Vehicle } from '../types/vehicles.types';
|
import { Vehicle } from '../types/vehicles.types';
|
||||||
@@ -123,7 +124,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
return list.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
}, [fuelLogs]);
|
}, [fuelLogs]);
|
||||||
|
|
||||||
const filteredRecords = useMemo(() => {
|
const filteredRecords = useMemo(() => {
|
||||||
@@ -286,7 +287,7 @@ export const VehicleDetailMobile: React.FC<VehicleDetailMobileProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
{/* Secondary line: Grade • Date • Type */}
|
{/* Secondary line: Grade • Date • Type */}
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.25 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.25 }}>
|
||||||
{rec.secondary || `${new Date(rec.date).toLocaleDateString()} • ${rec.type}`}
|
{rec.secondary || `${dayjs(rec.date).format('M/D/YYYY')} • ${rec.type}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useState, useEffect } from 'react';
|
import React, { useMemo, useState, useEffect } from 'react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Box, Typography, Button as MuiButton, Divider, FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableCell, TableBody, Dialog, DialogTitle, DialogContent, useMediaQuery, IconButton } from '@mui/material';
|
import { Box, Typography, Button as MuiButton, Divider, FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableCell, TableBody, Dialog, DialogTitle, DialogContent, useMediaQuery, IconButton } from '@mui/material';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
@@ -132,7 +133,7 @@ export const VehicleDetailPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
return list.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
}, [fuelLogs, documents]);
|
}, [fuelLogs, documents]);
|
||||||
|
|
||||||
const filteredRecords = useMemo(() => {
|
const filteredRecords = useMemo(() => {
|
||||||
@@ -490,7 +491,7 @@ export const VehicleDetailPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
{!isFuelLoading && !isDocumentsLoading && filteredRecords.map((rec) => (
|
{!isFuelLoading && !isDocumentsLoading && filteredRecords.map((rec) => (
|
||||||
<TableRow key={rec.id} hover sx={{ cursor: rec.type === 'Documents' ? 'default' : 'pointer' }} onClick={() => handleRowClick(rec.id, rec.type)}>
|
<TableRow key={rec.id} hover sx={{ cursor: rec.type === 'Documents' ? 'default' : 'pointer' }} onClick={() => handleRowClick(rec.id, rec.type)}>
|
||||||
<TableCell>{new Date(rec.date).toLocaleDateString()}</TableCell>
|
<TableCell>{dayjs(rec.date).format('M/D/YYYY')}</TableCell>
|
||||||
<TableCell>{rec.type}</TableCell>
|
<TableCell>{rec.type}</TableCell>
|
||||||
<TableCell>{rec.summary}</TableCell>
|
<TableCell>{rec.summary}</TableCell>
|
||||||
<TableCell align="right">{rec.amount || '—'}</TableCell>
|
<TableCell align="right">{rec.amount || '—'}</TableCell>
|
||||||
|
|||||||
Reference in New Issue
Block a user