From 6751766b0a75eb59d11209eff241f96739c8a649 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:55:21 -0600 Subject: [PATCH] chore: create AddReceiptDialog component with upload and camera options (refs #183) Co-Authored-By: Claude Opus 4.6 --- .../components/AddReceiptDialog.tsx | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 frontend/src/features/maintenance/components/AddReceiptDialog.tsx diff --git a/frontend/src/features/maintenance/components/AddReceiptDialog.tsx b/frontend/src/features/maintenance/components/AddReceiptDialog.tsx new file mode 100644 index 0000000..a2728d8 --- /dev/null +++ b/frontend/src/features/maintenance/components/AddReceiptDialog.tsx @@ -0,0 +1,272 @@ +/** + * @ai-summary Full-screen dialog with upload and camera options for receipt input + * @ai-context Replaces direct camera launch with upload-first pattern; both paths feed OCR pipeline + */ + +import React, { useRef, useState, useCallback } from 'react'; +import { + Dialog, + Box, + Typography, + Button, + IconButton, + Alert, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import CameraAltIcon from '@mui/icons-material/CameraAlt'; +import { + DEFAULT_ACCEPTED_FORMATS, + DEFAULT_MAX_FILE_SIZE, +} from '../../../shared/components/CameraCapture/types'; + +interface AddReceiptDialogProps { + open: boolean; + onClose: () => void; + onFileSelect: (file: File) => void; + onStartCamera: () => void; +} + +export const AddReceiptDialog: React.FC = ({ + open, + onClose, + onFileSelect, + onStartCamera, +}) => { + const inputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [error, setError] = useState(null); + + const validateFile = useCallback((file: File): string | null => { + const isValidType = DEFAULT_ACCEPTED_FORMATS.some((format) => { + if (format === 'image/heic' || format === 'image/heif') { + return ( + file.type === 'image/heic' || + file.type === 'image/heif' || + file.name.toLowerCase().endsWith('.heic') || + file.name.toLowerCase().endsWith('.heif') + ); + } + return file.type === format; + }); + + if (!isValidType) { + return 'Invalid file type. Accepted formats: JPEG, PNG, HEIC'; + } + + if (file.size > DEFAULT_MAX_FILE_SIZE) { + return `File too large. Maximum size: ${(DEFAULT_MAX_FILE_SIZE / (1024 * 1024)).toFixed(0)}MB`; + } + + return null; + }, []); + + const handleFile = useCallback( + (file: File) => { + setError(null); + const validationError = validateFile(file); + if (validationError) { + setError(validationError); + return; + } + onFileSelect(file); + }, + [validateFile, onFileSelect] + ); + + const handleInputChange = useCallback( + (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + handleFile(file); + } + if (inputRef.current) { + inputRef.current.value = ''; + } + }, + [handleFile] + ); + + const handleDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragging(false); + }, []); + + const handleDrop = useCallback( + (event: React.DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsDragging(false); + const file = event.dataTransfer.files[0]; + if (file) { + handleFile(file); + } + }, + [handleFile] + ); + + const handleClickUpload = useCallback(() => { + inputRef.current?.click(); + }, []); + + // Reset error state when dialog closes + const handleClose = useCallback(() => { + setError(null); + setIsDragging(false); + onClose(); + }, [onClose]); + + return ( + + {/* Header */} + + Add Receipt + + + + + + {/* Content */} + + {/* Drag-and-drop upload zone */} + + + + {isDragging ? 'Drop image here' : 'Drag and drop an image, or tap to browse'} + + + JPEG, PNG, HEIC -- up to 10MB + + + + {error && ( + + {error} + + )} + + {/* Divider with "or" */} + + + + or + + + + + {/* Take Photo button */} + + + + {/* Hidden file input */} + + + ); +};