feat: add inline supplier creation in invoice inbox and remove dead code

- Add inline new supplier form in InboxDetailDialog with pre-populated
  fields from AI extraction
- Support new_supplier payload in confirm endpoint to create suppliers
  with user-editable fields (type, org number, bankgiro, etc.)
- Improve line item amount calculation using cross-checked extraction totals
- Remove unused estimateProductValue function and LangChain imports
  from receipt-analyzer
- Add debug logging to invoice inbox confirm flow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-02-26 19:55:50 +01:00
co-authored by Claude Opus 4.6
parent 3f255ea201
commit 9cd950de88
4 changed files with 257 additions and 132 deletions
@@ -118,9 +118,17 @@ export default function InvoiceInboxWorkspace({ userId }: WorkspaceComponentProp
}
}
async function handleConfirm(itemId: string, supplierId?: string) {
const body: Record<string, string> = {}
if (supplierId) body.supplier_id = supplierId
async function handleConfirm(
itemId: string,
supplierId?: string,
newSupplierData?: import('@/components/extensions/general/invoice-inbox/InboxDetailDialog').NewSupplierData
) {
const body: Record<string, unknown> = {}
if (supplierId) {
body.supplier_id = supplierId
} else if (newSupplierData) {
body.new_supplier = newSupplierData
}
const res = await fetch(`/api/extensions/ext/invoice-inbox/inbox/${itemId}/confirm`, {
method: 'POST',
@@ -1,7 +1,7 @@
'use client'
import { useState } from 'react'
import type { InvoiceInboxItem, Supplier } from '@/types'
import { useState, useEffect } from 'react'
import type { InvoiceInboxItem, Supplier, SupplierType } from '@/types'
import type { InvoiceExtractionResult } from '@/extensions/general/invoice-inbox/types'
import {
Dialog,
@@ -13,6 +13,8 @@ import {
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
@@ -34,13 +36,24 @@ import {
getStatusVariant,
getConfidenceLabel,
} from '@/lib/extensions/invoice-inbox-utils'
import { Loader2, RefreshCw, Check, X } from 'lucide-react'
import { Loader2, RefreshCw, Check, X, ChevronDown } from 'lucide-react'
export interface NewSupplierData {
name: string
supplier_type: SupplierType
org_number: string
vat_number: string
bankgiro: string
plusgiro: string
default_expense_account: string
default_currency: string
}
interface InboxDetailDialogProps {
item: InvoiceInboxItem | null
open: boolean
onOpenChange: (open: boolean) => void
onConfirm: (itemId: string, supplierId?: string) => Promise<void>
onConfirm: (itemId: string, supplierId?: string, newSupplierData?: NewSupplierData) => Promise<void>
onReject: (itemId: string) => Promise<void>
onReprocess: (itemId: string) => Promise<void>
suppliers: Supplier[]
@@ -67,6 +80,34 @@ export default function InboxDetailDialog({
}: InboxDetailDialogProps) {
const [loading, setLoading] = useState<'confirm' | 'reject' | 'reprocess' | null>(null)
const [selectedSupplierId, setSelectedSupplierId] = useState<string | undefined>(undefined)
const [supplierFormOpen, setSupplierFormOpen] = useState(false)
const [newSupplier, setNewSupplier] = useState<NewSupplierData>({
name: '',
supplier_type: 'swedish_business',
org_number: '',
vat_number: '',
bankgiro: '',
plusgiro: '',
default_expense_account: '6200',
default_currency: 'SEK',
})
// Pre-populate supplier form when item changes
const extractionForEffect = item?.extracted_data as unknown as InvoiceExtractionResult | null
useEffect(() => {
if (!extractionForEffect?.supplier) return
const s = extractionForEffect.supplier
setNewSupplier({
name: s.name ?? '',
supplier_type: 'swedish_business',
org_number: s.orgNumber ?? '',
vat_number: s.vatNumber ?? '',
bankgiro: s.bankgiro ?? '',
plusgiro: s.plusgiro ?? '',
default_expense_account: '6200',
default_currency: extractionForEffect.invoice?.currency || 'SEK',
})
}, [extractionForEffect])
if (!item) return null
@@ -78,16 +119,25 @@ export default function InboxDetailDialog({
const matchedSupplierName = (item.supplier as { name?: string } | undefined)?.name
const supplierId = selectedSupplierId ?? item.matched_supplier_id ?? undefined
const isCreatingNewSupplier = !supplierId
const canConfirm = item.status === 'ready' && extraction != null
const canReprocess = item.status !== 'confirmed'
const canReject = item.status !== 'confirmed' && item.status !== 'rejected'
function updateSupplierField<K extends keyof NewSupplierData>(field: K, value: NewSupplierData[K]) {
setNewSupplier((prev) => ({ ...prev, [field]: value }))
}
async function handleAction(action: 'confirm' | 'reject' | 'reprocess') {
setLoading(action)
try {
if (action === 'confirm') {
await onConfirm(item!.id, supplierId)
await onConfirm(
item!.id,
supplierId,
isCreatingNewSupplier ? newSupplier : undefined
)
} else if (action === 'reject') {
await onReject(item!.id)
} else {
@@ -185,6 +235,82 @@ export default function InboxDetailDialog({
</SelectContent>
</Select>
</div>
{/* Inline new supplier form */}
{isCreatingNewSupplier && (
<div className="rounded-md border bg-muted/30 p-3 space-y-3">
<button
type="button"
className="flex w-full items-center justify-between text-sm font-medium"
onClick={() => setSupplierFormOpen((o) => !o)}
>
<span>Granska leverantörsuppgifter</span>
<ChevronDown className={`h-4 w-4 transition-transform ${supplierFormOpen ? 'rotate-180' : ''}`} />
</button>
{supplierFormOpen && (
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2 space-y-1">
<Label className="text-xs">Namn</Label>
<Input
value={newSupplier.name}
onChange={(e) => updateSupplierField('name', e.target.value)}
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Typ</Label>
<Select
value={newSupplier.supplier_type}
onValueChange={(v) => updateSupplierField('supplier_type', v as SupplierType)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="swedish_business">Svenskt företag</SelectItem>
<SelectItem value="eu_business">EU-företag</SelectItem>
<SelectItem value="non_eu_business">Utomeuropeiskt</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs">Org.nr</Label>
<Input
value={newSupplier.org_number}
onChange={(e) => updateSupplierField('org_number', e.target.value)}
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Bankgiro</Label>
<Input
value={newSupplier.bankgiro}
onChange={(e) => updateSupplierField('bankgiro', e.target.value)}
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Plusgiro</Label>
<Input
value={newSupplier.plusgiro}
onChange={(e) => updateSupplierField('plusgiro', e.target.value)}
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Kostnadskonto</Label>
<Input
value={newSupplier.default_expense_account}
onChange={(e) => updateSupplierField('default_expense_account', e.target.value)}
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Valuta</Label>
<Input
value={newSupplier.default_currency}
onChange={(e) => updateSupplierField('default_currency', e.target.value)}
/>
</div>
</div>
)}
</div>
)}
</div>
<Separator />