// screen-receipt.jsx — Foto Struk → AI → Auto-fill Transaksi // Uses Claude claude-sonnet-4-20250514 Vision via Anthropic API proxy const ANTHROPIC_KEY = window.INDIECASH_CONFIG?.anthropicKey || ''; function ScreenReceipt({ theme, accent, scenario, onOpenSheet, onTransactionAdded }) { const T = theme; const [step, setStep] = React.useState('idle'); // idle | processing | result | error const [previewUrl, setPreviewUrl] = React.useState(null); const [result, setResult] = React.useState(null); const [errorMsg, setErrorMsg] = React.useState(''); const [history, setHistory] = React.useState([]); const fileRef = React.useRef(null); const cameraRef = React.useRef(null); const CATEGORIES_LIST = Object.values(CATEGORIES).filter(c => c.kind === 'expense'); async function processImage(file) { if (!file) return; // Preview const url = URL.createObjectURL(file); setPreviewUrl(url); setStep('processing'); setResult(null); try { // Convert to base64 const base64 = await new Promise((res, rej) => { const reader = new FileReader(); reader.onload = () => res(reader.result.split(',')[1]); reader.onerror = rej; reader.readAsDataURL(file); }); const mediaType = file.type || 'image/jpeg'; const prompt = `Kamu adalah asisten pencatat keuangan Indonesia. Analisis struk/nota/bukti pembayaran ini. Ekstrak informasi berikut dan kembalikan HANYA JSON valid (tanpa markdown, tanpa penjelasan): { "merchant": "nama toko/merchant", "total": 150000, "tanggal": "2024-05-26", "items": [ {"nama": "nama item", "harga": 50000, "qty": 1} ], "kategori": "makan|belanja|transport|tagihan|hiburan|kesehatan|pendidikan|rumah|lainnya_ex", "metode_bayar": "cash|debit|kredit|qris|transfer", "catatan": "ringkasan singkat 1 kalimat dalam bahasa Indonesia", "confidence": 0.95 } Aturan: - total harus angka integer (dalam rupiah, tanpa titik/koma) - tanggal format YYYY-MM-DD, jika tidak ada pakai hari ini - kategori pilih yang paling sesuai dari daftar yang ada - confidence antara 0-1 (seberapa yakin kamu dengan hasilnya) - Jika struk tidak terbaca/bukan struk, kembalikan {"error": "Bukan struk yang valid"}`; // Call backend proxy — key stays on server, never exposed to browser const response = await fetch('/api/receipt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: base64, media_type: mediaType }), }); if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.error || `Server error ${response.status}`); } const proxyResult = await response.json(); if (!proxyResult.ok) throw new Error(proxyResult.error || 'Gagal memproses'); // proxyResult.data is already parsed JSON from server const parsed = proxyResult.data; if (parsed.error) throw new Error(parsed.error); setResult(parsed); setStep('result'); return; // skip the rest of the try block } catch (err) { setErrorMsg(err.message || 'Gagal memproses gambar'); setStep('error'); } } function handleFile(e) { const file = e.target.files?.[0]; if (file) processImage(file); } function confirmAndSave() { if (!result) return; // Add to history const entry = { id: Date.now(), merchant: result.merchant, total: result.total, kategori: result.kategori, tanggal: result.tanggal, catatan: result.catatan, previewUrl, savedAt: new Date().toISOString(), }; setHistory(h => [entry, ...h].slice(0, 10)); // Trigger add transaction sheet with pre-filled data if (onTransactionAdded) onTransactionAdded(entry); setStep('saved'); setTimeout(() => { setStep('idle'); setPreviewUrl(null); setResult(null); }, 2000); } function retry() { setStep('idle'); setPreviewUrl(null); setResult(null); setErrorMsg(''); } const catInfo = result ? (CATEGORIES[result.kategori] || CATEGORIES.lainnya_ex) : null; return (