Files
accounted/extensions/general/receipt-ocr/pages/scan/ScanReceiptPage.tsx
T
2026-02-21 11:39:45 +01:00

158 lines
4.6 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import ReceiptCamera from '../../components/ReceiptCamera'
import ReceiptReviewView from '../../components/ReceiptReviewView'
import TransactionMatcher from '../../components/TransactionMatcher'
import { Loader2 } from 'lucide-react'
import type { Receipt, ReceiptLineItem, ConfirmLineItemInput } from '@/types'
type PageState = 'camera' | 'uploading' | 'review' | 'match' | 'done'
export default function ScanReceiptPage() {
const router = useRouter()
const [pageState, setPageState] = useState<PageState>('camera')
const [uploadError, setUploadError] = useState<string | null>(null)
const [receipt, setReceipt] = useState<(Receipt & { line_items: ReceiptLineItem[] }) | null>(null)
// Handle image capture
const handleCapture = async (imageData: string, mimeType: string) => {
setPageState('uploading')
setUploadError(null)
try {
// Convert base64 to blob
const byteCharacters = atob(imageData)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: mimeType })
// Create form data
const formData = new FormData()
formData.append('image', blob, 'receipt.jpg')
// Upload and analyze
const response = await fetch('/api/extensions/receipt-ocr/upload', {
method: 'POST',
body: formData,
})
const data = await response.json()
if (response.ok && data.data) {
setReceipt(data.data)
setPageState('review')
} else {
setUploadError(data.error || 'Kunde inte analysera kvittot')
setPageState('camera')
}
} catch (error) {
console.error('Upload error:', error)
setUploadError('Nätverksfel. Försök igen.')
setPageState('camera')
}
}
// Handle close/cancel
const handleClose = () => {
router.push('/receipts')
}
// Handle confirm receipt
const handleConfirm = async (data: {
line_items: ConfirmLineItemInput[]
representation_persons?: number
representation_purpose?: string
}) => {
if (!receipt) return
const response = await fetch(`/api/extensions/receipt-ocr/${receipt.id}/confirm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (response.ok) {
const updatedData = await response.json()
setReceipt(updatedData.data)
setPageState('match')
} else {
const errorData = await response.json()
throw new Error(errorData.error || 'Kunde inte bekräfta kvitto')
}
}
// Handle match to transaction
const handleMatch = async (transactionId: string, confidence: number) => {
if (!receipt) return
const response = await fetch(`/api/extensions/receipt-ocr/${receipt.id}/match`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transaction_id: transactionId, match_confidence: confidence }),
})
if (response.ok) {
router.push('/receipts')
}
}
// Handle skip matching
const handleSkipMatch = () => {
router.push('/receipts')
}
// Render uploading state
if (pageState === 'uploading') {
return (
<div className="fixed inset-0 bg-background z-50 flex flex-col items-center justify-center">
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
<p className="text-lg font-medium">Analyserar kvitto...</p>
<p className="text-sm text-muted-foreground mt-1">
AI läser av artiklar och belopp
</p>
</div>
)
}
// Render review view
if (pageState === 'review' && receipt) {
return (
<ReceiptReviewView
receipt={receipt}
onConfirm={handleConfirm}
onCancel={handleClose}
onFindMatches={() => setPageState('match')}
/>
)
}
// Render match view
if (pageState === 'match' && receipt) {
return (
<TransactionMatcher
receipt={receipt}
onMatch={handleMatch}
onSkip={handleSkipMatch}
onClose={handleClose}
/>
)
}
// Render camera (default)
return (
<>
{uploadError && (
<div className="fixed top-4 left-4 right-4 z-[60] bg-destructive text-destructive-foreground p-3 rounded-lg text-sm">
{uploadError}
</div>
)}
<ReceiptCamera onCapture={handleCapture} onClose={handleClose} />
</>
)
}