diff --git a/app/(dashboard)/agent-inbox/page.tsx b/app/(dashboard)/agent-inbox/page.tsx deleted file mode 100644 index 8b45144f..00000000 --- a/app/(dashboard)/agent-inbox/page.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { notFound, redirect } from 'next/navigation' -import Link from 'next/link' -import { createClient } from '@/lib/supabase/server' -import { ensureInitialized } from '@/lib/init' -import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' -import { isAgentInboxEnabled } from '@/lib/ai/feature-flag' -import { requireCompanyId } from '@/lib/company/context' -import { PageHeader } from '@/components/ui/page-header' -import { Button } from '@/components/ui/button' -import { Card, CardContent } from '@/components/ui/card' -import { Sparkles } from 'lucide-react' -import AgentInbox from '@/components/agent-inbox/AgentInbox' -import type { AIProposal, AIRequest, InvoiceInboxItem, Transaction, DocumentAttachment, MatchProposalPayload } from '@/types' - -ensureInitialized() - -// Expanded card data the client component needs to render each proposal's -// context (receipt thumbnail + matched transaction summary). -export interface AgentInboxItemView { - proposal: AIProposal | null - request: AIRequest | null - inbox_item: InvoiceInboxItem & { document: DocumentAttachment | null } - transaction: Transaction | null -} - -export default async function AgentInboxPage() { - // Hard gate: extension not enabled at build time, OR the feature flag is - // off in this environment (prod by default) → 404. - if (!ENABLED_EXTENSION_IDS.has('ai-agent') || !isAgentInboxEnabled()) { - notFound() - } - - const supabase = await createClient() - const { data: { user } } = await supabase.auth.getUser() - if (!user) redirect('/login') - - const companyId = await requireCompanyId(supabase, user.id) - - // Soft gate: per-company toggle. If not enabled, show an empty-state - // pointing to settings rather than 404 — the extension exists, the user - // just hasn't opted in yet. - const { data: settings } = await supabase - .from('company_settings') - .select('ai_flow_enabled') - .eq('company_id', companyId) - .maybeSingle() - - if (!settings?.ai_flow_enabled) { - return ( -
- Aktivera AI-agenten under bokföringsinställningar. Varje transaktion blir då ett - granskningsförslag istället för automatisk bokföring. -
- -{activeTab === 'pending' - ? 'När din AI-agent skapar bokföring visas den här för granskning.' - : 'Historik för AI-agentens operationer visas här.'} + ? 'När en operation kräver godkännande visas den här för granskning.' + : 'Operationer du har godkänt eller avvisat visas här.'}
diff --git a/app/(dashboard)/receipts/page.tsx b/app/(dashboard)/receipts/page.tsx deleted file mode 100644 index 8fb39805..00000000 --- a/app/(dashboard)/receipts/page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { notFound, redirect } from 'next/navigation' -import { createClient } from '@/lib/supabase/server' -import { ensureInitialized } from '@/lib/init' -import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions' -import { requireCompanyId } from '@/lib/company/context' -import ReceiptsList from '@/components/receipts/ReceiptsList' -import type { InvoiceInboxItem, DocumentAttachment } from '@/types' - -ensureInitialized() - -export type ReceiptRow = InvoiceInboxItem & { document: DocumentAttachment | null } -export type ReceiptRowWithPreview = ReceiptRow & { preview_url: string | null } - -export default async function ReceiptsPage() { - // Hard gate: if the invoice-inbox extension isn't loaded, there's no - // upload pipeline and nothing would work here. - if (!ENABLED_EXTENSION_IDS.has('invoice-inbox')) notFound() - - const supabase = await createClient() - const { data: { user } } = await supabase.auth.getUser() - if (!user) redirect('/login') - - const companyId = await requireCompanyId(supabase, user.id) - - const { data } = await supabase - .from('invoice_inbox_items') - .select('*, document:document_attachments!document_id(*)') - .eq('company_id', companyId) - .eq('document_type', 'receipt') - .order('created_at', { ascending: false }) - .limit(200) - - const rows = (data ?? []) as ReceiptRow[] - - // Batch-sign all document paths so each row renders with a thumbnail. - // Supabase exposes createSignedUrls (plural) for exactly this use case. - const paths = rows - .map((r) => r.document?.storage_path) - .filter((p): p is string => Boolean(p)) - - const urlByPath = new Map- {settings.entity_type === 'aktiebolag' - ? 'Aktiebolag med omsättning över 3 MSEK måste använda faktureringsmetoden.' - : 'Kontantmetoden är tillgänglig för enskild firma med omsättning under 3 MSEK.'} + Kontantmetoden får användas om årlig nettoomsättning normalt är högst + 3 MSEK (BFL 5 kap. 2 §). Obetalda fordringar och skulder ska bokföras + vid räkenskapsårets utgång.
- När aktiv: varje ny banktransaktion blir ett AI-förslag du granskar i - agent-inkorgen. - Den automatiska bokföringen (≥80% regelmatchning) stängs av — inget bokförs - utan din bekräftelse. -
-- ${senderName ? `${senderName} ` : ''}behöver ett kvittounderlag för ${companyName} innan en transaktion kan bokföras. -
-- ${summaryLine || 'Kvitto utan extraherade uppgifter'} -
-Öppna agent-inkorgen och ladda upp en tydlig bild eller PDF av kvittot:
- -- Send in receipts. Utan källunderlag kan bokföringen inte slutföras enligt BFL 5 kap 7§. -
-` -} - -function buildText({ companyName, summaryLine, deepLink, senderName }: TemplateArgs): string { - return [ - 'Source documents required for receipt and transaction mapping.', - '', - `${senderName ? `${senderName} ` : ''}behöver ett kvittounderlag för ${companyName} innan en transaktion kan bokföras.`, - '', - summaryLine || 'Kvitto utan extraherade uppgifter', - '', - 'Öppna agent-inkorgen och ladda upp en tydlig bild eller PDF av kvittot:', - deepLink, - '', - 'Send in receipts. Utan källunderlag kan bokföringen inte slutföras enligt BFL 5 kap 7§.', - ].join('\n') -} diff --git a/app/api/ai/learning/remember/route.ts b/app/api/ai/learning/remember/route.ts deleted file mode 100644 index b6051611..00000000 --- a/app/api/ai/learning/remember/route.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { NextResponse } from 'next/server' -import { createClient } from '@/lib/supabase/server' -import { ensureInitialized } from '@/lib/init' -import { validateBody } from '@/lib/api/validate' -import { RememberLearningSchema } from '@/lib/api/schemas' -import { requireCompanyId } from '@/lib/company/context' -import { requireWritePermission } from '@/lib/auth/require-write' -import { calculateConfidence } from '@/lib/bookkeeping/counterparty-templates' -import { gateAgentInbox } from '@/lib/ai/feature-flag' -import type { AIProposal } from '@/types' - -ensureInitialized() - -/** - * POST /api/ai/learning/remember - * - * Called from the UI's learning-prompt dialog after a user edited and - * accepted an AI booking proposal. Upserts a categorization_templates row - * with source='ai_corrected' so next time's proposal for the same - * counterparty starts from the user's preference. - * - * This is the ONLY path that creates an ai_corrected template — the - * "silent learning" rule means every template with this source represents - * an explicit user choice. - */ -export async function POST(request: Request) { - const gate = gateAgentInbox() - if (gate) return gate - - const supabase = await createClient() - const { data: { user } } = await supabase.auth.getUser() - if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - - const writeCheck = await requireWritePermission(supabase, user.id) - if (!writeCheck.ok) return writeCheck.response - - const companyId = await requireCompanyId(supabase, user.id) - - const validation = await validateBody(request, RememberLearningSchema) - if (!validation.success) return validation.response - const { - proposal_id, - counterparty_name, - debit_account, - credit_account, - vat_treatment, - category, - } = validation.data - - // Verify the proposal is accepted + belongs to this company. - const { data: proposal } = await supabase - .from('ai_proposals') - .select('*') - .eq('id', proposal_id) - .eq('company_id', companyId) - .maybeSingle() - - if (!proposal) return NextResponse.json({ error: 'Not found' }, { status: 404 }) - const typed = proposal as AIProposal - if (typed.status !== 'accepted') { - return NextResponse.json( - { error: 'Endast accepterade förslag kan lagras som mall.' }, - { status: 400 } - ) - } - if (typed.step_type !== 'booking') { - return NextResponse.json( - { error: 'Endast bokföringssteget kan lagras som mall.' }, - { status: 400 } - ) - } - - // Upsert the template. Existing row for the same (user_id, counterparty_name) - // gets its source bumped up and occurrence incremented. - const { data: existing } = await supabase - .from('categorization_templates') - .select('*') - .eq('user_id', user.id) - .eq('counterparty_name', counterparty_name) - .maybeSingle() - - const today = new Date().toISOString().slice(0, 10) - - if (existing) { - const newOccurrence = existing.occurrence_count + 1 - await supabase - .from('categorization_templates') - .update({ - debit_account, - credit_account, - vat_treatment, - category, - source: 'ai_corrected', - occurrence_count: newOccurrence, - confidence: calculateConfidence(newOccurrence), - last_seen_date: today, - is_active: true, - }) - .eq('id', existing.id) - - return NextResponse.json({ data: { template_id: existing.id, updated: true } }) - } - - const { data: created, error: insertError } = await supabase - .from('categorization_templates') - .insert({ - user_id: user.id, - company_id: companyId, - counterparty_name, - counterparty_aliases: [counterparty_name], - debit_account, - credit_account, - vat_treatment, - category, - source: 'ai_corrected', - occurrence_count: 1, - confidence: calculateConfidence(1), - last_seen_date: today, - is_active: true, - }) - .select() - .single() - - if (insertError) return NextResponse.json({ error: insertError.message }, { status: 500 }) - - return NextResponse.json({ data: { template_id: created.id, updated: false } }) -} diff --git a/app/api/ai/proposals/[id]/accept/route.ts b/app/api/ai/proposals/[id]/accept/route.ts deleted file mode 100644 index 01d0c20e..00000000 --- a/app/api/ai/proposals/[id]/accept/route.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { NextResponse } from 'next/server' -import { createClient } from '@/lib/supabase/server' -import { eventBus } from '@/lib/events/bus' -import { ensureInitialized } from '@/lib/init' -import { validateBody } from '@/lib/api/validate' -import { AcceptProposalSchema } from '@/lib/api/schemas' -import { requireCompanyId } from '@/lib/company/context' -import { requireWritePermission } from '@/lib/auth/require-write' -import { reValidateProposal } from '@/lib/ai/proposals/re-validate' -import { applyProposal } from '@/lib/ai/proposals/apply' -import { bookkeepingErrorResponse } from '@/lib/bookkeeping/errors' -import { appendProcessingHistory } from '@/lib/processing-history/append' -import { gateAgentInbox } from '@/lib/ai/feature-flag' -import type { - AIProposal, - BookingProposalPayload, - InvoiceInboxItem, - MatchProposalPayload, -} from '@/types' - -ensureInitialized() - -/** - * POST /api/ai/proposals/[id]/accept - * - * Accept a pending proposal: - * 1. Optimistic lock on version to catch concurrent clicks. - * 2. Re-validate (period open, transaction still unbooked, accounts active). - * 3. Apply via lib/ai/proposals/apply.ts (engine call happens there). - * 4. Mark status='accepted', set applied_entry_id, bump version. - * 5. If `edits` provided and differ from proposal_json, record edit_diff - * and return a `learning_prompt` hint so the UI can ask - * "remember this booking for- När nya kvitton klassas i inkorgen kommer AI-förslagen att visas här. -
-- AI-agenten skapar förslag ett kvitto i taget. Nya kort dyker upp här automatiskt. -
-- - Kvittobild saknas — ladda upp i kvittodialogen innan du kan godkänna. -
- )} - -“{reasoning}”
- )} - {hasAlternatives && ( -| Konto | -Debet | -Kredit | -
|---|---|---|
| - {line.account_number}{' '} - {line.description} - | -- {line.debit_amount > 0 ? line.debit_amount.toFixed(2) : ''} - | -- {line.credit_amount > 0 ? line.credit_amount.toFixed(2) : ''} - | -
| - {payload.vat_treatment && Moms: {payload.vat_treatment}} - {payload.default_private && Privat uttag} - | -{totalDebit.toFixed(2)} | -{totalDebit.toFixed(2)} | -
{req.message}
-Kvitton
-- {summary.receiptQueue.pending_review_count > 0 - ? `${summary.receiptQueue.pending_review_count} att granska` - : `${summary.receiptQueue.unmatched_receipts_count} omatchade`} -
-- {item.match_reasoning} -
- )} -Din fakturainkorg
-{inboxAddress.address}
-
+ {inboxAddress.address}
+
+ + {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}
++ Kunde inte läsa text — manuell registrering krävs. +
+ )} +- Aktiebolag med omsättning över 3 MSEK per år måste använda faktureringsmetoden. -
- )} ++ Kontantmetoden får användas om årlig nettoomsättning normalt är högst + 3 MSEK (BFL 5 kap. 2 §). +
- AI läste {v.claude_total != null ? formatCurrency(v.claude_total, currency) : '—'}, OCR läste{' '} - {v.ocr_total != null ? formatCurrency(v.ocr_total, currency) : '—'}. Granska bilden innan du godkänner. -
- ) -} - -// Optimistic placeholder shown in the list while a manual upload is in -// flight. The upload handler is synchronous (classify + store + insert -// happen before the response returns), so a 5-10 s gap otherwise leaves the -// user staring at nothing. We insert a fake row here so the UI shows a real -// card immediately and router.refresh() replaces it with the persisted row. -interface PendingUpload { - key: string - file_name: string - size_bytes: number - mime_type: string -} - -export default function ReceiptsList({ initialItems }: { initialItems: ReceiptRowWithPreview[] }) { - const [items, setItems] = useState(initialItems) - const [uploading, setUploading] = useState(false) - const [pendingUploads, setPendingUploads] = useState- Ladda upp ett kvitto (PDF, JPG, PNG, HEIC eller WebP) så tar AI hand om resten. -
-{row.error_message}
- )} -- Dokumentet från inkorgen länkas automatiskt till verifikationen. -
-{supplierName || merchantName}
- )} - {totals?.totals?.total != null && ( -- {formatCurrency(totals.totals.total)} -
- )} - > - ) - })()} - {currentTransaction.matched_inbox_item.suggested_template_id && ( -- Mall: {currentTransaction.matched_inbox_item.suggested_template_id} -
- )} -{transaction.description}
-{formatDate(transaction.date)}
- {hasDocumentMatch && ( -{formatDate(transaction.date)}