'use client'; import { useRef, useState, useEffect } from 'react'; type CategorySuggestion = { categoryId: number; categoryName: string; path: string; confidence: 'high' | 'medium' | 'low'; usedFallback: boolean; }; type ParsedItem = { rawName: string; quantity: number; unit: string; price?: number | null; brand?: string | null; origin?: string | null; matchedProductId?: number; matchedProductName?: string; suggestedProductId?: number; suggestedProductName?: string; categorySuggestion?: CategorySuggestion; }; type Product = { id: number; name: string; canonicalName: string | null }; type Category = { id: number; name: string; parentId: number | null }; type RowState = { productSearch: string; selectedCategoryId: number | ''; // för manuellt val vid none utan AI rawName: string; quantity: number; unit: string; price?: number | null; selectedProductId: number | ''; selectedProductName: string; checked: boolean; saveAlias: boolean; editQty: string; editUnit: string; editBrand: string; editOrigin: string; editComment: string; matchSource: 'alias' | 'suggestion' | 'manual' | 'none'; categorySuggestion?: CategorySuggestion; }; const UNITS = ['st', 'kg', 'g', 'l', 'dl', 'cl', 'ml', 'förp', 'pak', 'burk', 'flaska']; export default function ReceiptImportClient({ isAdmin }: { isAdmin: boolean }) { const fileRef = useRef(null); // Debug: log role on mount useEffect(() => { // eslint-disable-next-line no-console console.log('ReceiptImportClient: isAdmin =', isAdmin); }, [isAdmin]); const [preview, setPreview] = useState(null); const [parsing, setParsing] = useState(false); const [saving, setSaving] = useState(false); const [rows, setRows] = useState([]); const [allProducts, setAllProducts] = useState([]); const [allCategories, setAllCategories] = useState([]); const [productsLoading, setProductsLoading] = useState(true); const [productsError, setProductsError] = useState(null); const [error, setError] = useState(null); const [savedCount, setSavedCount] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [creatingProduct, setCreatingProduct] = useState(null); useEffect(() => { fetch('/api/products') .then(async (r) => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then((data) => { if (Array.isArray(data)) { setAllProducts(data); } else { setProductsError('Oväntat format från produktlistan'); } }) .catch((e) => setProductsError(`Kunde inte ladda produktlistan: ${e.message}`)) .finally(() => setProductsLoading(false)); fetch('/api/categories') .then((r) => r.json()) .then((data) => { if (Array.isArray(data)) setAllCategories(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, editBrand: item.brand ?? '', editOrigin: item.origin ?? '', editComment: '', matchSource: 'alias', productSearch: item.matchedProductName ?? '', selectedCategoryId: '', }; } 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, editBrand: item.brand ?? '', editOrigin: item.origin ?? '', editComment: '', matchSource: 'suggestion', productSearch: item.suggestedProductName ?? '', selectedCategoryId: '', }; } 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, editBrand: item.brand ?? '', editOrigin: item.origin ?? '', editComment: '', matchSource: 'none', categorySuggestion: item.categorySuggestion, productSearch: '', selectedCategoryId: '', }; }), ); } 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 handleCreateProduct = async (i: number) => { const row = rows[i]; setCreatingProduct(i); setError(null); // eslint-disable-next-line no-console console.log('handleCreateProduct: isAdmin =', isAdmin, 'endpoint = /api/products'); try { // Admin skapar aktiv produkt direkt const createRes = await fetch('/api/products', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: row.rawName }), }); if (!createRes.ok) { const e = await createRes.json().catch(() => ({})); throw new Error(e.message ?? `HTTP ${createRes.status}`); } const product = await createRes.json() as { id: number; name: string; canonicalName: string | null }; // Sätt kategori: AI-förslag har prioritet, annars manuellt val const categoryId = row.categorySuggestion?.categoryId ?? (row.selectedCategoryId !== '' ? row.selectedCategoryId : null); if (categoryId) { await fetch(`/api/products/${product.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ categoryId }), }); } // Uppdatera produktlistan lokalt const newProduct = { id: product.id, name: product.name, canonicalName: product.canonicalName }; setAllProducts((prev) => [...prev, newProduct].sort((a, b) => (a.canonicalName ?? a.name).localeCompare(b.canonicalName ?? b.name, 'sv'))); // Markera raden som matchad updateRow(i, { selectedProductId: product.id, selectedProductName: product.canonicalName ?? product.name, productSearch: product.canonicalName ?? product.name, checked: true, matchSource: 'manual', saveAlias: false, }); } catch (err) { setError(`Kunde inte skapa produkt: ${err instanceof Error ? err.message : String(err)}`); } finally { setCreatingProduct(null); } }; const handleSuggestProduct = async (i: number) => { const row = rows[i]; setCreatingProduct(i); setError(null); // eslint-disable-next-line no-console console.log('handleSuggestProduct: isAdmin =', isAdmin, 'endpoint = /api/products/pending'); try { // Användare skapar ett pending-förslag const createRes = await fetch('/api/products/pending', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: row.rawName }), }); if (!createRes.ok) { const e = await createRes.json().catch(() => ({})); throw new Error(e.message ?? `HTTP ${createRes.status}`); } const product = await createRes.json() as { id: number; name: string; canonicalName: string | null }; // Sätt kategori om vald/föreslagen const categoryId = row.categorySuggestion?.categoryId ?? (row.selectedCategoryId !== '' ? row.selectedCategoryId : null); if (categoryId) { await fetch(`/api/products/${product.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ categoryId }), }); } // Lägg till i lokal lista (men markera som pending) const newProduct = { id: product.id, name: product.name, canonicalName: product.canonicalName }; setAllProducts((prev) => [...prev, newProduct].sort((a, b) => (a.canonicalName ?? a.name).localeCompare(b.canonicalName ?? b.name, 'sv'))); // Markera raden — pending = kan läggas till i inventariet men väntar på admin-godkännande updateRow(i, { selectedProductId: product.id, selectedProductName: product.canonicalName ?? product.name, productSearch: product.canonicalName ?? product.name, checked: true, matchSource: 'manual', saveAlias: false, }); } catch (err) { setError(`Kunde inte föreslå produkt: ${err instanceof Error ? err.message : String(err)}`); } finally { setCreatingProduct(null); } }; const handleSave = async () => { const toSave = rows.filter((r) => r.checked && r.selectedProductId !== ''); if (toSave.length === 0) return; setSaving(true); setError(null); try { const inventoryResults = 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, receiptName: r.rawName, brand: r.editBrand.trim() || undefined, origin: r.editOrigin.trim() || undefined, comment: r.editComment.trim() || undefined, }), }), ), ); const failedInventory = inventoryResults.find((r) => !r.ok); if (failedInventory) { const e = await failedInventory.json().catch(() => ({})); throw new Error(e.message ?? `Inventory HTTP ${failedInventory.status}`); } await Promise.all( 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 (err) { setError(`Sparning misslyckades: ${err instanceof Error ? err.message : 'Okänt fel'}. Försök igen.`); } finally { setSaving(false); } }; const checkedCount = rows.filter((r) => r.checked && r.selectedProductId !== '').length; // Bygg en lista av kategorier under "Övrigt" + "Övrigt" självt som fallback const ovrigtOptions = (() => { const ovrigt = allCategories.find((c) => c.name === 'Övrigt' && c.parentId === null); if (!ovrigt) return allCategories.slice(0, 20); // fallback: visa alla const children = allCategories.filter((c) => c.parentId === ovrigt.id).sort((a, b) => a.name.localeCompare(b.name, 'sv')); return [ovrigt, ...children]; })(); 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
)}
{productsError && (

⚠️ {productsError}

)} {preview && rows.length === 0 && ( )} {error && (

{error}

)} {savedCount !== null && (

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

)} {rows.length > 0 && (
{!isAdmin && (
Tips: Om en vara saknas kan du klicka Föreslå ny vara — varan läggs till i inventariet och skickas för granskning av en administratör.
)}

Identifierade varor ({rows.length})

🟢 Känd = automatiskt markerad · 🟠 Förslag = markera för att inkludera · {isAdmin ? 'Sök eller skapa ny produkt' : 'Sök eller föreslå ny vara'}
{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} )}
{ const val = e.target.value; updateRow(i, { productSearch: val }); // Hitta exakt match const match = allProducts.find( (p) => (p.canonicalName ?? p.name) === val ); if (match) { updateRow(i, { productSearch: val, selectedProductId: match.id, selectedProductName: match.canonicalName ?? match.name, checked: true, matchSource: row.matchSource === 'alias' ? 'alias' : 'manual', saveAlias: row.matchSource !== 'alias', }); } else { updateRow(i, { productSearch: val, selectedProductId: '', selectedProductName: '', checked: false, }); } }} placeholder={productsLoading ? 'Laddar produkter...' : 'Sök produkt...'} disabled={productsLoading} style={{ width: '100%', padding: '0.35rem 0.5rem', border: `1px solid ${row.selectedProductId !== '' ? '#22c55e' : '#ced4da'}`, borderRadius: '6px', fontSize: '0.9rem', boxSizing: 'border-box' }} /> {allProducts.map((p) => (
updateRow(i, { editQty: e.target.value })} style={{ padding: '0.35rem 0.5rem', border: '1px solid #ced4da', borderRadius: '6px', fontSize: '0.9rem' }} />
updateRow(i, { editBrand: e.target.value })} placeholder="Märke / leverantör (valfritt)" style={{ padding: '0.3rem 0.5rem', border: '1px solid #ced4da', borderRadius: '6px', fontSize: '0.82rem', color: '#555' }} /> updateRow(i, { editOrigin: e.target.value })} placeholder="Ursprungsland (valfritt)" style={{ padding: '0.3rem 0.5rem', border: '1px solid #ced4da', borderRadius: '6px', fontSize: '0.82rem', color: '#555' }} />
updateRow(i, { editComment: e.target.value })} placeholder="Kommentar, t.ex. styckning, kvalitet... (valfritt)" style={{ width: '100%', padding: '0.3rem 0.5rem', border: '1px solid #ced4da', borderRadius: '6px', fontSize: '0.82rem', color: '#555', boxSizing: 'border-box' }} />
{row.categorySuggestion && row.matchSource === 'none' && (
AI-förslag: {row.categorySuggestion.path} {row.categorySuggestion.usedFallback && (osäker)}
{isAdmin ? ( ) : ( )}
)} {row.matchSource === 'none' && !row.categorySuggestion && (
{isAdmin ? ( ) : ( )}
)} {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' }; }