feat: add context-aware document delete from vehicle screen (refs #31)

- Created DeleteDocumentConfirmDialog with context-aware messaging:
  - Primary vehicle with no shares: Full delete
  - Shared vehicle: Remove association only
  - Primary vehicle with shares: Full delete (affects all)
- Integrated documents display in VehicleDetailPage records table
- Added delete button per document with 44px touch target
- Document deletion uses appropriate backend calls based on context
- Mobile-friendly dialog with responsive design

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-14 19:41:52 -06:00
parent b71e2cff3c
commit bdb329f7c3
2 changed files with 233 additions and 8 deletions

View File

@@ -4,12 +4,13 @@
import React, { useMemo, useState, useEffect } from 'react';
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 } 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 ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditIcon from '@mui/icons-material/Edit';
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation';
import BuildIcon from '@mui/icons-material/Build';
import DeleteIcon from '@mui/icons-material/Delete';
import { Vehicle } from '../types/vehicles.types';
import { vehiclesApi } from '../api/vehicles.api';
import { Card } from '../../../shared-minimal/components/Card';
@@ -23,6 +24,9 @@ import { FuelLogForm } from '../../fuel-logs/components/FuelLogForm';
// Unit conversions now handled by backend
import { fuelLogsApi } from '../../fuel-logs/api/fuel-logs.api';
import { OwnershipCostsList } from '../../ownership-costs';
import { useDocumentsByVehicle, useDeleteDocument, useRemoveVehicleFromDocument } from '../../documents/hooks/useDocuments';
import { DeleteDocumentConfirmDialog } from '../../documents/components/DeleteDocumentConfirmDialog';
import type { DocumentRecord } from '../../documents/types/documents.types';
const DetailField: React.FC<{
label: string;
@@ -53,9 +57,14 @@ export const VehicleDetailPage: React.FC = () => {
const [recordFilter, setRecordFilter] = useState<'All' | 'Fuel Logs' | 'Maintenance' | 'Documents'>('All');
const { fuelLogs, isLoading: isFuelLoading } = useFuelLogs(id);
const { data: documents, isLoading: isDocumentsLoading } = useDocumentsByVehicle(id);
const { mutateAsync: deleteDocument } = useDeleteDocument();
const { mutateAsync: removeVehicleFromDocument } = useRemoveVehicleFromDocument();
const queryClient = useQueryClient();
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
const [showAddDialog, setShowAddDialog] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [documentToDelete, setDocumentToDelete] = useState<DocumentRecord | null>(null);
const isSmallScreen = useMediaQuery('(max-width:600px)');
// Unit conversions now handled by backend
@@ -105,8 +114,24 @@ export const VehicleDetailPage: React.FC = () => {
list.push({ id: log.id, type: 'Fuel Logs', date: log.dateTime, summary, amount });
}
}
// Add documents to records
if (documents && Array.isArray(documents)) {
for (const doc of documents) {
const parts: string[] = [];
parts.push(doc.title);
parts.push(doc.documentType.charAt(0).toUpperCase() + doc.documentType.slice(1));
if (doc.expirationDate) {
parts.push(`Expires: ${new Date(doc.expirationDate).toLocaleDateString()}`);
}
const summary = parts.join(' • ');
const date = doc.issuedDate || doc.createdAt;
list.push({ id: doc.id, type: 'Documents', date, summary, amount: undefined });
}
}
return list.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}, [fuelLogs]);
}, [fuelLogs, documents]);
const filteredRecords = useMemo(() => {
if (recordFilter === 'All') return records;
@@ -208,6 +233,46 @@ export const VehicleDetailPage: React.FC = () => {
const log = (fuelLogs as FuelLogResponse[] | undefined)?.find(l => l.id === recId) || null;
setEditingLog(log);
}
// Documents are handled via delete button, not row click
};
const handleDeleteDocumentClick = (docId: string, event: React.MouseEvent) => {
event.stopPropagation(); // Prevent row click
const doc = documents?.find(d => d.id === docId) || null;
if (doc) {
setDocumentToDelete(doc);
setDeleteDialogOpen(true);
}
};
const handleDeleteConfirm = async (fullDelete: boolean) => {
if (!documentToDelete || !id) return;
try {
if (fullDelete) {
// Full delete
await deleteDocument(documentToDelete.id);
console.log('Document deleted permanently');
} else {
// Remove vehicle association only
await removeVehicleFromDocument({ docId: documentToDelete.id, vehicleId: id });
console.log('Document removed from vehicle');
}
// Invalidate queries to refresh data
queryClient.invalidateQueries({ queryKey: ['documents-by-vehicle', id] });
queryClient.invalidateQueries({ queryKey: ['documents'] });
setDeleteDialogOpen(false);
setDocumentToDelete(null);
} catch (err) {
console.error('Error deleting/removing document:', err);
}
};
const handleDeleteCancel = () => {
setDeleteDialogOpen(false);
setDocumentToDelete(null);
};
const handleCloseEdit = () => setEditingLog(null);
@@ -398,29 +463,43 @@ export const VehicleDetailPage: React.FC = () => {
<TableCell>Type</TableCell>
<TableCell>Summary</TableCell>
<TableCell align="right">Amount</TableCell>
<TableCell align="right" sx={{ width: 80 }}>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isFuelLoading && (
{(isFuelLoading || isDocumentsLoading) && (
<TableRow>
<TableCell colSpan={4}>
<TableCell colSpan={5}>
<Typography color="text.secondary">Loading records</Typography>
</TableCell>
</TableRow>
)}
{!isFuelLoading && filteredRecords.length === 0 && (
{!isFuelLoading && !isDocumentsLoading && filteredRecords.length === 0 && (
<TableRow>
<TableCell colSpan={4}>
<TableCell colSpan={5}>
<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)}>
{!isFuelLoading && !isDocumentsLoading && filteredRecords.map((rec) => (
<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>{rec.type}</TableCell>
<TableCell>{rec.summary}</TableCell>
<TableCell align="right">{rec.amount || '—'}</TableCell>
<TableCell align="right">
{rec.type === 'Documents' && (
<IconButton
size="small"
color="error"
onClick={(e) => handleDeleteDocumentClick(rec.id, e)}
sx={{ minWidth: 44, minHeight: 44 }}
aria-label="Delete document"
>
<DeleteIcon fontSize="small" />
</IconButton>
)}
</TableCell>
</TableRow>
))}
</TableBody>
@@ -461,6 +540,15 @@ export const VehicleDetailPage: React.FC = () => {
</Box>
</DialogContent>
</Dialog>
{/* Delete Document Confirmation Dialog */}
<DeleteDocumentConfirmDialog
open={deleteDialogOpen}
onClose={handleDeleteCancel}
onConfirm={handleDeleteConfirm}
document={documentToDelete}
vehicleId={id || null}
/>
</Card>
</Box>
);