Släpp filen för att ladda upp
'use client' import { useState, useCallback, useEffect, useRef, useMemo } from 'react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Skeleton } from '@/components/ui/skeleton' import AiFilledIndicator from '@/components/ui/ai-filled-indicator' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { useToast } from '@/components/ui/use-toast' import { Inbox, Upload, Mail, FileText, Copy, RotateCcw, Trash2, Check, Loader2, AlertTriangle, ArrowRight, Plus, Link2, Search, Circle, X, ChevronDown, Sparkles, } from 'lucide-react' import Link from 'next/link' import { cn, formatCurrency } from '@/lib/utils' import { createClient } from '@/lib/supabase/client' import { useCapability } from '@/contexts/CompanyContext' import { CAPABILITY } from '@/lib/entitlements/keys' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' import type { InvoiceExtractionResult } from '@/types' import BookDirectlyDialog from '@/components/extensions/general/BookDirectlyDialog' import TransactionMatchPicker from '@/components/inbox/TransactionMatchPicker' import { useAgentSheet } from '@/components/agent/AgentSheetProvider' type AccountingMethod = 'accrual' | 'cash' // ── Types ──────────────────────────────────────────────────── interface InboxItem { id: string status: 'received' | 'error' source: 'email' | 'upload' created_at: string email_from: string | null email_subject: string | null email_received_at: string | null document_id: string | null extracted_data: InvoiceExtractionResult | null matched_supplier_id: string | null matched_transaction_id: string | null created_supplier_invoice_id: string | null created_journal_entry_id: string | null error_message: string | null // True when AI extraction was skipped — either because the upload caller // passed skip_extraction=true (MCP/agent path) or because the server's // page-count gate skipped a PDF above the auto-extract limit (issue #553). // Distinct from status='error' (extraction failed) and from extracted_data // having empty fields (extraction ran but found nothing). extraction_skipped: boolean // Set client-side only while a manual upload is in flight. Replaced by a // real server-side row once the AI extraction completes. isPlaceholder?: boolean fileName?: string } interface InboxAddress { address: string local_part: string status: string } // ── Helpers ────────────────────────────────────────────────── function timeAgo(iso: string): string { const ms = Date.now() - new Date(iso).getTime() const min = Math.floor(ms / 60000) if (min < 1) return 'nyss' if (min < 60) return `${min} min sedan` const h = Math.floor(min / 60) if (h < 24) return `${h} h sedan` const d = Math.floor(h / 24) if (d < 30) return `${d} d sedan` return new Date(iso).toLocaleDateString('sv-SE') } function pickAmount(item: InboxItem): number | null { return item.extracted_data?.totals?.total ?? null } function pickCurrency(item: InboxItem): string { return item.extracted_data?.invoice?.currency ?? 'SEK' } function pickSupplierName(item: InboxItem): string | null { return item.extracted_data?.supplier?.name ?? null } // True when extraction produced at least one usable field. Distinguishes a // deterministically-parsed underlag (fields present — render the editable // list) from an item whose extracted_data is null/empty (AI never ran, or ran // and found nothing). Currency is ignored because emptyExtraction() seeds it // to 'SEK', so it is never a sign that extraction actually happened. function hasAnyExtractedField(data: InvoiceExtractionResult | null): boolean { if (!data) return false const s = data.supplier const inv = data.invoice const t = data.totals return Boolean( s?.name || s?.orgNumber || s?.vatNumber || s?.bankgiro || s?.plusgiro || inv?.invoiceNumber || inv?.invoiceDate || inv?.dueDate || inv?.paymentReference || t?.subtotal != null || t?.vatAmount != null || t?.total != null || (data.lineItems?.length ?? 0) > 0 || (data.vatBreakdown?.length ?? 0) > 0 ) } // Lifecycle stage of an inbox item. Single source of truth shared by the list // filter, the count pills, and the row icons so they never drift apart. // // Precedence mirrors the FieldsRail: a booked item (supplier invoice OR a // direct journal entry) is done and drops out of the active inbox. A // matched-but-unbooked item is "linked" — it STAYS in the inbox as its own // category because the bank payment still needs booking (a document attached // to a transaction is not the same as a booked one). An extraction failure is // "error"; everything else needs a first action. type InboxStatus = 'needs_action' | 'linked' | 'booked' | 'error' function deriveInboxStatus(item: InboxItem): InboxStatus { if (item.created_supplier_invoice_id || item.created_journal_entry_id) return 'booked' if (item.matched_transaction_id) return 'linked' if (item.status === 'error') return 'error' return 'needs_action' } // ── Skeleton ───────────────────────────────────────────────── // Mirrors the live layout (top bar + 3-pane card) so the transition from // the route-level loading.tsx to data-loaded content has no visible reflow. // Keep in sync with app/(dashboard)/e/[sector]/[slug]/loading.tsx. function WorkspaceSkeleton() { return (
{inboxAddress.address}
>
) : (
)}
Släpp filen för att ladda upp
Underlagen samlas här — från mail eller filuppladdning — och kan matchas mot bankhändelser eller bokföras direkt.
Gratis under beta för Open-användare. Ingår senare i Pro-planen.{' '} Se priser →
{step.title}
{!isDone && ({step.hint}
)}…eller maila underlagen till din inkorgsadress.
Välj en post i listan för att matcha eller bokföra.
)}{onActivateInbox ? 'Aktivera din inkorgsadress' : 'Välj ett dokument från listan'}
{onActivateInbox ? 'Ditt bolag får en unik e-postadress som leverantörer kan skicka fakturor till.' : 'Eller dra och släpp en fil var som helst på sidan för att ladda upp.'}
Fel vid bearbetning
{item.error_message}
Uppgradera för att låta accounted läsa av leverantör, belopp och moms automatiskt. Du kan fortfarande fylla i fälten manuellt eller koppla dokumentet till en transaktion nedan.
Momsfördelning
Posten är kopplad till en leverantörsfaktura — fälten kan inte ändras.
)}