feat: add form integration, tier gating, and receipt display (refs #153)
- Add tier-gated "Scan Receipt" button to MaintenanceRecordForm - Wire useMaintenanceReceiptOcr hook with CameraCapture and ReviewModal - Auto-populate form fields from accepted OCR results via setValue - Upload receipt as document and pass receiptDocumentId on record create - Show receipt thumbnail + "View Receipt" button in edit dialog - Add receipt indicator chip on records list rows - Add receiptDocumentId and receiptDocument to frontend types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @ai-summary Edit dialog for maintenance records
|
||||
* @ai-context Mobile-friendly dialog with proper form handling
|
||||
* @ai-summary Edit dialog for maintenance records with linked receipt display
|
||||
* @ai-context Mobile-friendly dialog with proper form handling and receipt thumbnail/view
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
MenuItem,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import ReceiptIcon from '@mui/icons-material/Receipt';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||
@@ -32,6 +34,7 @@ import {
|
||||
} from '../types/maintenance.types';
|
||||
import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup';
|
||||
import { useVehicles } from '../../vehicles/hooks/useVehicles';
|
||||
import { documentsApi } from '../../documents/api/documents.api';
|
||||
import type { Vehicle } from '../../vehicles/types/vehicles.types';
|
||||
|
||||
interface MaintenanceRecordEditDialogProps {
|
||||
@@ -53,7 +56,10 @@ export const MaintenanceRecordEditDialog: React.FC<MaintenanceRecordEditDialogPr
|
||||
|
||||
const vehiclesQuery = useVehicles();
|
||||
const vehicles = vehiclesQuery.data;
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const isSmallScreen = useMediaQuery('(max-width:600px)');
|
||||
const [receiptThumbnailUrl, setReceiptThumbnailUrl] = useState<string | null>(null);
|
||||
|
||||
// Reset form when record changes
|
||||
useEffect(() => {
|
||||
@@ -76,6 +82,45 @@ export const MaintenanceRecordEditDialog: React.FC<MaintenanceRecordEditDialogPr
|
||||
}
|
||||
}, [record]);
|
||||
|
||||
// Load receipt thumbnail when record has a linked receipt document
|
||||
useEffect(() => {
|
||||
if (!record?.receiptDocument?.documentId) {
|
||||
setReceiptThumbnailUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let revoked = false;
|
||||
documentsApi.download(record.receiptDocument.documentId).then((blob) => {
|
||||
if (!revoked) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
setReceiptThumbnailUrl(url);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error('[MaintenanceRecordEditDialog] Failed to load receipt thumbnail:', err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
revoked = true;
|
||||
if (receiptThumbnailUrl) {
|
||||
URL.revokeObjectURL(receiptThumbnailUrl);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [record?.receiptDocument?.documentId]);
|
||||
|
||||
const handleViewReceipt = async () => {
|
||||
if (!record?.receiptDocument?.documentId) return;
|
||||
try {
|
||||
const blob = await documentsApi.download(record.receiptDocument.documentId);
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, '_blank');
|
||||
// Revoke after a delay to allow the new tab to load
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
||||
} catch (err) {
|
||||
console.error('[MaintenanceRecordEditDialog] Failed to open receipt:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof UpdateMaintenanceRecordRequest, value: any) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
@@ -182,6 +227,76 @@ export const MaintenanceRecordEditDialog: React.FC<MaintenanceRecordEditDialogPr
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* Linked Receipt Display */}
|
||||
{record.receiptDocument && (
|
||||
<Grid item xs={12}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
alignItems: isMobile ? 'center' : 'center',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'action.hover',
|
||||
}}
|
||||
>
|
||||
{receiptThumbnailUrl ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={receiptThumbnailUrl}
|
||||
alt="Receipt"
|
||||
sx={{
|
||||
width: isMobile ? 64 : 80,
|
||||
height: isMobile ? 64 : 80,
|
||||
borderRadius: 1,
|
||||
objectFit: 'cover',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: isMobile ? 64 : 80,
|
||||
height: isMobile ? 64 : 80,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'grey.200',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<ReceiptIcon color="action" />
|
||||
</Box>
|
||||
)}
|
||||
<Box sx={{ flex: 1, minWidth: 0, textAlign: isMobile ? 'center' : 'left' }}>
|
||||
<Typography variant="body2" fontWeight={500}>
|
||||
Linked Receipt
|
||||
</Typography>
|
||||
{record.receiptDocument.fileName && (
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
{record.receiptDocument.fileName}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<ReceiptIcon />}
|
||||
onClick={handleViewReceipt}
|
||||
sx={{
|
||||
minHeight: 44,
|
||||
width: isMobile ? '100%' : 'auto',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
View Receipt
|
||||
</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Category */}
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth>
|
||||
|
||||
Reference in New Issue
Block a user