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-summary Edit dialog for maintenance records with linked receipt display
|
||||||
* @ai-context Mobile-friendly dialog with proper form handling
|
* @ai-context Mobile-friendly dialog with proper form handling and receipt thumbnail/view
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
@@ -19,7 +19,9 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import ReceiptIcon from '@mui/icons-material/Receipt';
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||||
@@ -32,6 +34,7 @@ import {
|
|||||||
} from '../types/maintenance.types';
|
} from '../types/maintenance.types';
|
||||||
import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup';
|
import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup';
|
||||||
import { useVehicles } from '../../vehicles/hooks/useVehicles';
|
import { useVehicles } from '../../vehicles/hooks/useVehicles';
|
||||||
|
import { documentsApi } from '../../documents/api/documents.api';
|
||||||
import type { Vehicle } from '../../vehicles/types/vehicles.types';
|
import type { Vehicle } from '../../vehicles/types/vehicles.types';
|
||||||
|
|
||||||
interface MaintenanceRecordEditDialogProps {
|
interface MaintenanceRecordEditDialogProps {
|
||||||
@@ -53,7 +56,10 @@ export const MaintenanceRecordEditDialog: React.FC<MaintenanceRecordEditDialogPr
|
|||||||
|
|
||||||
const vehiclesQuery = useVehicles();
|
const vehiclesQuery = useVehicles();
|
||||||
const vehicles = vehiclesQuery.data;
|
const vehicles = vehiclesQuery.data;
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const isSmallScreen = useMediaQuery('(max-width:600px)');
|
const isSmallScreen = useMediaQuery('(max-width:600px)');
|
||||||
|
const [receiptThumbnailUrl, setReceiptThumbnailUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
// Reset form when record changes
|
// Reset form when record changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -76,6 +82,45 @@ export const MaintenanceRecordEditDialog: React.FC<MaintenanceRecordEditDialogPr
|
|||||||
}
|
}
|
||||||
}, [record]);
|
}, [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) => {
|
const handleInputChange = (field: keyof UpdateMaintenanceRecordRequest, value: any) => {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -182,6 +227,76 @@ export const MaintenanceRecordEditDialog: React.FC<MaintenanceRecordEditDialogPr
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</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 */}
|
{/* Category */}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @ai-summary Form component for creating maintenance records
|
* @ai-summary Form component for creating maintenance records with receipt OCR integration
|
||||||
* @ai-context Mobile-first responsive design with proper validation
|
* @ai-context Mobile-first responsive design with tier-gated receipt scanning, mirrors FuelLogForm OCR pattern
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
CircularProgress,
|
CircularProgress,
|
||||||
Typography,
|
Typography,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
|
Dialog,
|
||||||
|
Backdrop,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
@@ -36,6 +38,13 @@ import {
|
|||||||
CreateMaintenanceRecordRequest,
|
CreateMaintenanceRecordRequest,
|
||||||
getCategoryDisplayName,
|
getCategoryDisplayName,
|
||||||
} from '../types/maintenance.types';
|
} from '../types/maintenance.types';
|
||||||
|
import { useMaintenanceReceiptOcr } from '../hooks/useMaintenanceReceiptOcr';
|
||||||
|
import { MaintenanceReceiptReviewModal } from './MaintenanceReceiptReviewModal';
|
||||||
|
import { ReceiptCameraButton } from '../../fuel-logs/components/ReceiptCameraButton';
|
||||||
|
import { CameraCapture } from '../../../shared/components/CameraCapture';
|
||||||
|
import { useTierAccess } from '../../../core/hooks/useTierAccess';
|
||||||
|
import { UpgradeRequiredDialog } from '../../../shared-minimal/components/UpgradeRequiredDialog';
|
||||||
|
import { documentsApi } from '../../documents/api/documents.api';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
@@ -58,6 +67,29 @@ export const MaintenanceRecordForm: React.FC = () => {
|
|||||||
const { createRecord, isRecordMutating } = useMaintenanceRecords();
|
const { createRecord, isRecordMutating } = useMaintenanceRecords();
|
||||||
const [selectedCategory, setSelectedCategory] = useState<MaintenanceCategory | null>(null);
|
const [selectedCategory, setSelectedCategory] = useState<MaintenanceCategory | null>(null);
|
||||||
|
|
||||||
|
// Tier access check for receipt scan feature
|
||||||
|
const { hasAccess } = useTierAccess();
|
||||||
|
const hasReceiptScanAccess = hasAccess('maintenance.receiptScan');
|
||||||
|
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
|
||||||
|
|
||||||
|
// Receipt OCR integration
|
||||||
|
const {
|
||||||
|
isCapturing,
|
||||||
|
isProcessing,
|
||||||
|
result: ocrResult,
|
||||||
|
receiptImageUrl,
|
||||||
|
error: ocrError,
|
||||||
|
startCapture,
|
||||||
|
cancelCapture,
|
||||||
|
processImage,
|
||||||
|
acceptResult,
|
||||||
|
reset: resetOcr,
|
||||||
|
updateField,
|
||||||
|
} = useMaintenanceReceiptOcr();
|
||||||
|
|
||||||
|
// Store captured file for document upload on submit
|
||||||
|
const [capturedReceiptFile, setCapturedReceiptFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -89,8 +121,69 @@ export const MaintenanceRecordForm: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [watchedCategory, setValue]);
|
}, [watchedCategory, setValue]);
|
||||||
|
|
||||||
|
// Wrap processImage to also save file reference
|
||||||
|
const handleCaptureImage = async (file: File, croppedFile?: File) => {
|
||||||
|
setCapturedReceiptFile(croppedFile || file);
|
||||||
|
await processImage(file, croppedFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle accepting OCR results and populating the form
|
||||||
|
const handleAcceptOcrResult = () => {
|
||||||
|
const mappedFields = acceptResult();
|
||||||
|
if (!mappedFields) return;
|
||||||
|
|
||||||
|
// Populate form fields from OCR result
|
||||||
|
if (mappedFields.category) {
|
||||||
|
setValue('category', mappedFields.category);
|
||||||
|
setSelectedCategory(mappedFields.category);
|
||||||
|
}
|
||||||
|
if (mappedFields.subtypes && mappedFields.subtypes.length > 0) {
|
||||||
|
setValue('subtypes', mappedFields.subtypes);
|
||||||
|
}
|
||||||
|
if (mappedFields.date) {
|
||||||
|
setValue('date', mappedFields.date);
|
||||||
|
}
|
||||||
|
if (mappedFields.cost !== undefined) {
|
||||||
|
setValue('cost', mappedFields.cost as any);
|
||||||
|
}
|
||||||
|
if (mappedFields.shopName) {
|
||||||
|
setValue('shop_name', mappedFields.shopName);
|
||||||
|
}
|
||||||
|
if (mappedFields.odometerReading !== undefined) {
|
||||||
|
setValue('odometer_reading', mappedFields.odometerReading as any);
|
||||||
|
}
|
||||||
|
if (mappedFields.notes) {
|
||||||
|
setValue('notes', mappedFields.notes);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle retaking photo
|
||||||
|
const handleRetakePhoto = () => {
|
||||||
|
resetOcr();
|
||||||
|
setCapturedReceiptFile(null);
|
||||||
|
startCapture();
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: FormData) => {
|
const onSubmit = async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
|
let receiptDocumentId: string | undefined;
|
||||||
|
|
||||||
|
// Upload receipt as document if we have a captured file
|
||||||
|
if (capturedReceiptFile) {
|
||||||
|
try {
|
||||||
|
const doc = await documentsApi.create({
|
||||||
|
vehicleId: data.vehicle_id,
|
||||||
|
documentType: 'manual',
|
||||||
|
title: `Maintenance Receipt - ${new Date(data.date).toLocaleDateString()}`,
|
||||||
|
});
|
||||||
|
await documentsApi.upload(doc.id, capturedReceiptFile);
|
||||||
|
receiptDocumentId = doc.id;
|
||||||
|
} catch (uploadError) {
|
||||||
|
console.error('Failed to upload receipt document:', uploadError);
|
||||||
|
toast.error('Receipt upload failed, but the record will be saved without the receipt.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload: CreateMaintenanceRecordRequest = {
|
const payload: CreateMaintenanceRecordRequest = {
|
||||||
vehicleId: data.vehicle_id,
|
vehicleId: data.vehicle_id,
|
||||||
category: data.category as MaintenanceCategory,
|
category: data.category as MaintenanceCategory,
|
||||||
@@ -100,6 +193,7 @@ export const MaintenanceRecordForm: React.FC = () => {
|
|||||||
cost: data.cost ? Number(data.cost) : undefined,
|
cost: data.cost ? Number(data.cost) : undefined,
|
||||||
shopName: data.shop_name || undefined,
|
shopName: data.shop_name || undefined,
|
||||||
notes: data.notes || undefined,
|
notes: data.notes || undefined,
|
||||||
|
receiptDocumentId,
|
||||||
};
|
};
|
||||||
|
|
||||||
await createRecord(payload);
|
await createRecord(payload);
|
||||||
@@ -117,6 +211,7 @@ export const MaintenanceRecordForm: React.FC = () => {
|
|||||||
notes: '',
|
notes: '',
|
||||||
});
|
});
|
||||||
setSelectedCategory(null);
|
setSelectedCategory(null);
|
||||||
|
setCapturedReceiptFile(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create maintenance record:', error);
|
console.error('Failed to create maintenance record:', error);
|
||||||
toast.error('Failed to add maintenance record');
|
toast.error('Failed to add maintenance record');
|
||||||
@@ -140,6 +235,31 @@ export const MaintenanceRecordForm: React.FC = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader title="Add Maintenance Record" />
|
<CardHeader title="Add Maintenance Record" />
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{/* Receipt Scan Button */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
mb: 3,
|
||||||
|
pb: 2,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReceiptCameraButton
|
||||||
|
onClick={() => {
|
||||||
|
if (!hasReceiptScanAccess) {
|
||||||
|
setShowUpgradeDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startCapture();
|
||||||
|
}}
|
||||||
|
disabled={isProcessing || isRecordMutating}
|
||||||
|
variant="button"
|
||||||
|
locked={!hasReceiptScanAccess}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
{/* Vehicle Selection */}
|
{/* Vehicle Selection */}
|
||||||
@@ -374,6 +494,89 @@ export const MaintenanceRecordForm: React.FC = () => {
|
|||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Camera Capture Modal */}
|
||||||
|
<Dialog
|
||||||
|
open={isCapturing}
|
||||||
|
onClose={cancelCapture}
|
||||||
|
fullScreen
|
||||||
|
PaperProps={{
|
||||||
|
sx: { backgroundColor: 'black' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CameraCapture
|
||||||
|
onCapture={handleCaptureImage}
|
||||||
|
onCancel={cancelCapture}
|
||||||
|
guidanceType="receipt"
|
||||||
|
allowCrop={true}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* OCR Processing Overlay */}
|
||||||
|
<Backdrop
|
||||||
|
open={isProcessing}
|
||||||
|
sx={{
|
||||||
|
color: '#fff',
|
||||||
|
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
<Typography variant="body1">Extracting receipt data...</Typography>
|
||||||
|
</Backdrop>
|
||||||
|
|
||||||
|
{/* OCR Review Modal */}
|
||||||
|
{ocrResult && (
|
||||||
|
<MaintenanceReceiptReviewModal
|
||||||
|
open={!!ocrResult}
|
||||||
|
extractedFields={ocrResult.extractedFields}
|
||||||
|
receiptImageUrl={receiptImageUrl}
|
||||||
|
categorySuggestion={ocrResult.categorySuggestion}
|
||||||
|
onAccept={handleAcceptOcrResult}
|
||||||
|
onRetake={handleRetakePhoto}
|
||||||
|
onCancel={() => {
|
||||||
|
resetOcr();
|
||||||
|
setCapturedReceiptFile(null);
|
||||||
|
}}
|
||||||
|
onFieldEdit={updateField}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Upgrade Required Dialog for Receipt Scan */}
|
||||||
|
<UpgradeRequiredDialog
|
||||||
|
featureKey="maintenance.receiptScan"
|
||||||
|
open={showUpgradeDialog}
|
||||||
|
onClose={() => setShowUpgradeDialog(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* OCR Error Display */}
|
||||||
|
{ocrError && (
|
||||||
|
<Dialog open={!!ocrError} onClose={resetOcr} maxWidth="xs">
|
||||||
|
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
OCR Error
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
{ocrError}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
||||||
|
<Button onClick={startCapture} variant="contained">
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
resetOcr();
|
||||||
|
setCapturedReceiptFile(null);
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Edit, Delete } from '@mui/icons-material';
|
import { Edit, Delete, Receipt } from '@mui/icons-material';
|
||||||
import {
|
import {
|
||||||
MaintenanceRecordResponse,
|
MaintenanceRecordResponse,
|
||||||
getCategoryDisplayName,
|
getCategoryDisplayName,
|
||||||
@@ -136,6 +136,15 @@ export const MaintenanceRecordsList: React.FC<MaintenanceRecordsListProps> = ({
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{record.receiptDocument && (
|
||||||
|
<Chip
|
||||||
|
icon={<Receipt fontSize="small" />}
|
||||||
|
label="Receipt"
|
||||||
|
size="small"
|
||||||
|
color="info"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{record.notes && (
|
{record.notes && (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export interface MaintenanceRecord {
|
|||||||
cost?: number;
|
cost?: number;
|
||||||
shopName?: string;
|
shopName?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
receiptDocumentId?: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -105,6 +106,15 @@ export interface CreateMaintenanceRecordRequest {
|
|||||||
cost?: number;
|
cost?: number;
|
||||||
shopName?: string;
|
shopName?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
receiptDocumentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receipt document metadata returned on GET
|
||||||
|
export interface ReceiptDocumentMeta {
|
||||||
|
documentId: string;
|
||||||
|
fileName: string;
|
||||||
|
contentType: string;
|
||||||
|
storageKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateMaintenanceRecordRequest {
|
export interface UpdateMaintenanceRecordRequest {
|
||||||
@@ -148,6 +158,7 @@ export interface UpdateScheduleRequest {
|
|||||||
// Response types (camelCase)
|
// Response types (camelCase)
|
||||||
export interface MaintenanceRecordResponse extends MaintenanceRecord {
|
export interface MaintenanceRecordResponse extends MaintenanceRecord {
|
||||||
subtypeCount: number;
|
subtypeCount: number;
|
||||||
|
receiptDocument?: ReceiptDocumentMeta | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MaintenanceScheduleResponse extends MaintenanceSchedule {
|
export interface MaintenanceScheduleResponse extends MaintenanceSchedule {
|
||||||
|
|||||||
Reference in New Issue
Block a user