feat: Expand OCR with fuel receipt scanning and maintenance extraction (#129) #147

Merged
egullickson merged 26 commits from issue-129-expand-ocr-fuel-receipt-maintenance into main 2026-02-13 02:25:55 +00:00
2 changed files with 55 additions and 24 deletions
Showing only changes of commit bc91fbad79 - Show all commits

View File

@@ -19,6 +19,8 @@ import { useFuelLogs } from '../hooks/useFuelLogs';
import { useUserSettings } from '../hooks/useUserSettings';
import { useReceiptOcr } from '../hooks/useReceiptOcr';
import { useGeolocation } from '../../stations/hooks/useGeolocation';
import { useTierAccess } from '../../../core/hooks/useTierAccess';
import { UpgradeRequiredDialog } from '../../../shared-minimal/components/UpgradeRequiredDialog';
import { CameraCapture } from '../../../shared/components/CameraCapture';
import { CreateFuelLogRequest, FuelType } from '../types/fuel-logs.types';
@@ -48,6 +50,11 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
// Get user location for nearby station search
const { coordinates: userLocation } = useGeolocation();
// Tier access check for receipt scan feature
const { hasAccess } = useTierAccess();
const hasReceiptScanAccess = hasAccess('fuelLog.receiptScan');
const [showUpgradeDialog, setShowUpgradeDialog] = useState(false);
// Receipt OCR integration
const {
isCapturing,
@@ -217,9 +224,16 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
}}
>
<ReceiptCameraButton
onClick={startCapture}
onClick={() => {
if (!hasReceiptScanAccess) {
setShowUpgradeDialog(true);
return;
}
startCapture();
}}
disabled={isProcessing || isLoading}
variant="button"
locked={!hasReceiptScanAccess}
/>
</Box>
@@ -436,6 +450,13 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial
/>
)}
{/* Upgrade Required Dialog for Receipt Scan */}
<UpgradeRequiredDialog
featureKey="fuelLog.receiptScan"
open={showUpgradeDialog}
onClose={() => setShowUpgradeDialog(false)}
/>
{/* OCR Error Display */}
{ocrError && (
<Dialog open={!!ocrError} onClose={resetOcr} maxWidth="xs">

View File

@@ -7,6 +7,7 @@ import React from 'react';
import { Button, IconButton, Tooltip, useTheme, useMediaQuery } from '@mui/material';
import CameraAltIcon from '@mui/icons-material/CameraAlt';
import ReceiptIcon from '@mui/icons-material/Receipt';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
export interface ReceiptCameraButtonProps {
/** Called when user clicks to start capture */
@@ -17,6 +18,8 @@ export interface ReceiptCameraButtonProps {
variant?: 'icon' | 'button' | 'auto';
/** Size of the button */
size?: 'small' | 'medium' | 'large';
/** Whether the feature is locked behind a tier gate */
locked?: boolean;
}
export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
@@ -24,6 +27,7 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
disabled = false,
variant = 'auto',
size = 'medium',
locked = false,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
@@ -31,28 +35,30 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
// Determine display variant
const displayVariant = variant === 'auto' ? (isMobile ? 'icon' : 'button') : variant;
const tooltipTitle = locked ? 'Upgrade to Pro to scan receipts' : 'Scan Receipt';
if (displayVariant === 'icon') {
return (
<Tooltip title="Scan Receipt">
<Tooltip title={tooltipTitle}>
<span>
<IconButton
onClick={onClick}
disabled={disabled}
color="primary"
size={size}
aria-label="Scan receipt with camera"
aria-label={locked ? 'Scan receipt (Pro feature)' : 'Scan receipt with camera'}
sx={{
backgroundColor: 'primary.light',
color: 'primary.contrastText',
backgroundColor: locked ? 'action.disabledBackground' : 'primary.light',
color: locked ? 'text.secondary' : 'primary.contrastText',
'&:hover': {
backgroundColor: 'primary.main',
backgroundColor: locked ? 'action.hover' : 'primary.main',
},
'&.Mui-disabled': {
backgroundColor: 'action.disabledBackground',
},
}}
>
<CameraAltIcon />
{locked ? <LockOutlinedIcon /> : <CameraAltIcon />}
</IconButton>
</span>
</Tooltip>
@@ -60,23 +66,27 @@ export const ReceiptCameraButton: React.FC<ReceiptCameraButtonProps> = ({
}
return (
<Button
onClick={onClick}
disabled={disabled}
variant="outlined"
color="primary"
size={size}
startIcon={<ReceiptIcon />}
endIcon={<CameraAltIcon />}
sx={{
borderStyle: 'dashed',
'&:hover': {
borderStyle: 'solid',
},
}}
>
Scan Receipt
</Button>
<Tooltip title={locked ? tooltipTitle : ''}>
<span>
<Button
onClick={onClick}
disabled={disabled}
variant="outlined"
color={locked ? 'inherit' : 'primary'}
size={size}
startIcon={locked ? <LockOutlinedIcon /> : <ReceiptIcon />}
endIcon={locked ? undefined : <CameraAltIcon />}
sx={{
borderStyle: 'dashed',
'&:hover': {
borderStyle: 'solid',
},
}}
>
{locked ? 'Scan Receipt (Pro)' : 'Scan Receipt'}
</Button>
</span>
</Tooltip>
);
};