'use client'; import { useRef, useState, useEffect } from 'react'; type ParsedItem = { rawName: string; quantity: number; unit: string; price?: number | null; matchedProductId?: number; matchedProductName?: string; suggestedProductId?: number; suggestedProductName?: string; }; type Product = { id: number; name: string; canonicalName: string | null }; type RowState = { rawName: string; quantity: number; unit: string; price?: number | null; selectedProductId: number | ''; selectedProductName: string; checked: boolean; saveAlias: boolean; editQty: string; editUnit: string; matchSource: 'alias' | 'suggestion' | 'manual' | 'none'; }; const UNITS = ['st', 'kg', 'g', 'l', 'dl', 'cl', 'ml', 'förp', 'pak', 'burk', 'flaska']; export default function ReceiptImportClient() { const fileRef = useRef(null); const [preview, setPreview] = useState(null); const [parsing, setParsing] = useState(false); const [saving, setSaving] = useState(false); const [rows, setRows] = useState([]); const [allProducts, setAllProducts] = useState([]); const [error, setError] = useState(null); const [savedCount, setSavedCount] = useState(null); const [selectedFile, setSelectedFile] = useState(null); useEffect(() => { fetch('/api/products') .then((r) => r.json()) .then((data) => { if (Array.isArray(data)) setAllProducts(data); }) .catch(() => {}); }, []); const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setSelectedFile(file); setPreview(file.type === 'application/pdf' ? 'pdf' : URL.createObjectURL(file)); setRows([]); setError(null); setSavedCount(null); }; const handleParse = async () => { if (!selectedFile) return; setParsing(true); setError(null); try { const fd = new FormData(); fd.append('file', selectedFile); const res = await fetch('/api/receipt-import-proxy', { method: 'POST', body: fd }); if (!res.ok) { const e = await res.json().catch(() => ({ message: 'Okänt fel' })); throw new Error(e.message ?? 'Servern svarade med fel'); } const items: ParsedItem[] = await res.json(); setRows( items.map((item): RowState => { if (item.matchedProductId) { return { rawName: item.rawName, quantity: item.quantity, unit: item.unit, price: item.price, selectedProductId: item.matchedProductId, selectedProductName: item.matchedProductName ?? '', checked: true, saveAlias: false, editQty: String(item.quantity), editUnit: item.unit, matchSource: 'alias', }; } if (item.suggestedProductId) { return { rawName: item.rawName, quantity: item.quantity, unit: item.unit, price: item.price, selectedProductId: item.suggestedProductId, selectedProductName: item.suggestedProductName ?? '', checked: false, saveAlias: false, editQty: String(item.quantity), editUnit: item.unit, matchSource: 'suggestion', }; } return { rawName: item.rawName, quantity: item.quantity, unit: item.unit, price: item.price, selectedProductId: '', selectedProductName: '', checked: false, saveAlias: false, editQty: String(item.quantity), editUnit: item.unit, matchSource: 'none', }; }), ); } catch (err) { setError(err instanceof Error ? err.message : 'Kunde inte tolka kvittot'); } finally { setParsing(false); } }; const updateRow = (i: number, patch: Partial) => { setRows((prev) => prev.map((r, idx) => (idx === i ? { ...r, ...patch } : r))); }; const handleProductSelect = (i: number, productId: string) => { if (!productId) { updateRow(i, { selectedProductId: '', selectedProductName: '', checked: false, matchSource: 'none' }); return; } const prod = allProducts.find((p) => p.id === Number(productId)); if (prod) { updateRow(i, { selectedProductId: prod.id, selectedProductName: prod.canonicalName ?? prod.name, checked: true, matchSource: 'manual', saveAlias: true, }); } }; const handleSave = async () => { const toSave = rows.filter((r) => r.checked && r.selectedProductId !== ''); if (toSave.length === 0) return; setSaving(true); setError(null); try { await Promise.all([ ...toSave.map((r) => fetch('/api/inventory', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productId: r.selectedProductId, quantity: parseFloat(r.editQty) || r.quantity, unit: r.editUnit, brand: r.rawName, }), }), ), ...toSave .filter((r) => r.saveAlias && r.selectedProductId !== '') .map((r) => fetch('/api/receipt-alias-proxy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ receiptName: r.rawName, productId: r.selectedProductId, }), }), ), ]); setSavedCount(toSave.length); setRows([]); setPreview(null); setSelectedFile(null); if (fileRef.current) fileRef.current.value = ''; } catch { setError('Något gick fel vid sparning. Försök igen.'); } finally { setSaving(false); } }; const checkedCount = rows.filter((r) => r.checked && r.selectedProductId !== '').length; const sourceLabel = (src: RowState['matchSource']) => { if (src === 'alias') return { text: 'Känd vara', color: '#27ae60' }; if (src === 'suggestion') return { text: 'Förslag', color: '#e67e22' }; if (src === 'manual') return { text: 'Manuellt vald', color: '#0070f3' }; return null; }; return (
fileRef.current?.click()} > {preview === 'pdf' ? (
📄
{selectedFile?.name}
PDF-kvitto valt
) : preview ? ( // eslint-disable-next-line @next/next/no-img-element Kvittoförhandsgranskning ) : (
📷
Fotografera eller välj kvitto
Klicka för att välja bild (JPEG, PNG, WebP) eller PDF
)}
{preview && rows.length === 0 && ( )} {error && (

{error}

)} {savedCount !== null && (

✓ {savedCount} {savedCount === 1 ? 'vara lades till' : 'varor lades till'} i inventariet.

)} {rows.length > 0 && (

Identifierade varor ({rows.length})

Grön = känd koppling · Orange = förslag · Välj manuellt om inget förslag
{rows.map((row, i) => { const label = sourceLabel(row.matchSource); return (
updateRow(i, { checked: e.target.checked })} style={{ width: '18px', height: '18px', cursor: row.selectedProductId !== '' ? 'pointer' : 'not-allowed' }} /> {row.rawName} {label && ( {label.text} )}
updateRow(i, { editQty: e.target.value })} style={{ padding: '0.35rem 0.5rem', border: '1px solid #ced4da', borderRadius: '6px', fontSize: '0.9rem' }} />
{row.selectedProductId !== '' && row.matchSource !== 'alias' && ( )}
); })}
)}
); } function primaryBtn(disabled: boolean): React.CSSProperties { return { padding: '0.6rem 1.25rem', background: disabled ? '#aaa' : '#0070f3', color: '#fff', border: 'none', borderRadius: '6px', cursor: disabled ? 'not-allowed' : 'pointer', fontWeight: 600, fontSize: '0.95rem' }; }