feat: comprehensive UI design audit and normalization (#28)
* feat: import system improvements, INK2 fix, and Swedish text corrections - SIE parser: Windows-1252 and CP437 encoding detection and decoding - Bank file parser: add Nordea Business (Företag) CSV format - Bank file parser: improve format detection for SEB, Länsförsäkringar, generic CSV - INK2 engine: calculate årets resultat (7222) from income statement for open fiscal years - Dashboard: parallel Supabase queries, simplified dashboard page - Fix Swedish characters (å, ä, ö) in BAS data descriptions, validation messages, AI consent disclosures - Import wizard UI improvements across all steps - Migration: add 'bas_range' match type to sie_account_mappings constraint - Extensive new tests for SIE parser encoding and bank file parser Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: arcim migration wizard UX fixes, Sentry setup, and extension scaffolding Arcim migration wizard improvements: - Progress bar now excludes non-interactive steps (migrating/result) - Fix OAuth text to match target="_blank" behavior (new tab, not redirect) - Display month names instead of "Månad X" in preview - Fix Swedish typo "förifylla" in no-company-info message - Replace native checkboxes with shadcn Switch in options step - Add ConfirmationDialog before starting migration - Show progress percentage during migration - Add "Nästa steg" guidance and navigation links in result step - Add "Försök igen" button in error state (returns to options) - Add Bokio company ID help text (GUID from URL) - Add Fortnox integration add-on hint on connection failure Also includes: SIE import system improvements, INK2 fixes, Swedish text corrections, Sentry error tracking setup, and arcim-migration extension scaffolding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address PR review feedback - Fix OAuth error recovery blank page (restore provider from URL params) - Pass real userId to MigrationWizard instead of empty string - Remove ~50 debug console.log statements from sie-import.ts - Fix comment referencing account 3740 → 3741 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: comprehensive UI design audit and normalization Dashboard audit: - Fix muted-foreground contrast (4.31:1 → 5.08:1) for WCAG AA - Add prefers-reduced-motion media query for all animations - Replace border-l-2 accent anti-pattern with subtle full-border colors - Add aria-expanded to toggle buttons, role="status" to live counters - Fix touch targets on deadline buttons (28px → 36px) - Vary section spacing for rhythm (mb-12/mb-10/mb-8) - Remove unused imports and dead code Transactions audit + hardening: - Add pagination (200 per page) with "Ladda fler" button - Replace height animation with transform-only exit animation - Show batch progress in floating action bar during processing - Fix batch bar mobile overlap (bottom-20 on mobile) - Replace clickable badges with proper button elements - Add safe area padding to fullscreen swipe view - Add response.ok check to suggestion fetch - Add truncation to invoice number buttons Invoicing audit: - Remove border-l-4 accent pattern from invoice cards - Replace string concatenation with cn() utility Systemic sweep (34 files): - All page headings: font-bold → font-display font-medium (Fraunces) - All stat numbers: font-bold → font-display font-medium tabular-nums - All hard-coded blue/amber/emerald colors → design tokens - Remove all dark mode overrides (tokens handle automatically) - Tint pure white card background to 99% Design context added to CLAUDE.md with brand personality, aesthetic direction, and 5 design principles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: bookkeeping flow audit — design system, accessibility, UX - Replace raw <select> with shadcn Select component (JournalEntryForm) - Add confirmation dialog for account deletion (ChartOfAccountsManager) - Remove console.error from production code (JournalEntryList, JournalEntryForm) - Fix contradictory h-7/min-h-[44px] button sizing → h-10 (ChartOfAccountsManager) - Increase BAS catalog "Lägg till" touch target h-7 → h-9 - Improve loading state with spinner (JournalEntryList) - Improve empty state with icon, description, and guidance (JournalEntryList) - Add response.ok check on journal entry fetch - Add aria-expanded to entry expand buttons - Add tabular-nums to desktop debit/credit columns Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: onboarding and empty state improvements Onboarding: - Replace font-serif with font-display (Fraunces) for brand consistency - Remove console.error calls from production code Empty states: - Fix broken /transactions/new link in EmptyTransactions (route doesn't exist) - Add actionHref fallback to EmptyCustomers when no onAction prop provided - Improve EmptyTransactions description copy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clarify Swedish UX copy — terminology, errors, descriptions Terminology consistency: - "Försenad" → "Förfallen" for overdue invoices (customers/[id]) - "bokföringsorder" → actionable description in bookkeeping page - "verifikation har bifogats" → "underlag har bifogats" in doc warning - "Fortsätt ändå" → "Bokför utan underlag" (specific action) Error messages — replace generic "Fel" + "Något gick fel" with specific: - "Något gick fel vid bokföring" → "Transaktionen kunde inte bokföras" - "Något gick fel vid matchning" → "Transaktionen kunde inte matchas" - "Kunde inte hämta X" → "Kunde inte ladda X" + recovery hint - Add "Försök igen" guidance to all error toasts Page descriptions — replace redundant with actionable: - Invoices: "Skapa och hantera" → "Skicka, följ betalningar, skapa kreditnotor" - Bookkeeping: list of features → actionable description Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: design critique — dashboard affordance, reports description Dashboard: - Add ChevronRight indicator to clickable summary cards (Att få betalt, Koppla bank) to distinguish from static cards - Add cursor-pointer to linked cards Reports: - Replace feature list description with actionable guidance "Huvudbok, grundbok..." → "Generera skattedeklarationer..." Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: replace generic "Fel" error toasts with specific messages Deadlines: 5 generic "Fel" → specific per-action titles (create, toggle, edit, delete, load) Expenses detail: 5 generic "Fel" → specific per-action titles (load, approve, pay, credit, delete) Expenses new: 3 generic "Fel" → instructional validation messages (supplier name, supplier selection, invoice number) Customers: 1 generic "Fel" → specific load error with recovery hint All error toasts now follow pattern: title = what failed, description = how to recover Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: replace all remaining generic "Fel" error toasts (37 instances) Systematic sweep across 12 dashboard pages replacing generic title: 'Fel' with context-specific error titles: - Load errors: "Kunde inte ladda [resurs]" - Action errors: "[Åtgärd] misslyckades" - Validation: "[Fält] saknas" Every error toast now tells the user what failed without needing to read the description. Recovery hints added where missing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: import flow — normalize stat typography, remove console.warn - Replace font-bold with font-display font-medium on 13 stat numbers across SIEPreviewStep, BankFilePreviewStep, BankFileConfirmStep, ImportResultStep (missed by systemic sweep since these are in components/import/, not app/(dashboard)/) - Add tabular-nums to stat numbers displaying counts/currency - Remove console.warn in ArcimMigrationWorkspace Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: final cleanup — console statements, remaining font-bold stats Remove production console statements: - Step1EntityType: remove debug console.warn (dead code after onNext) - TransactionBookingDialog: remove console.error on doc link failure - JournalEntryAttachments: remove 3 console.error calls Normalize remaining font-bold stat displays: - SwipeCategorizationView: 3 instances (completion, amount displays) - NEDeclarationView: yearly result heading + value Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review feedback loadMoreTransactions: add inbox item enrichment matching fetchTransactions - Paginated transactions now fetch invoice_inbox_items in parallel - Fixes missing document indicator, template suggestions, and inbox match card for transactions loaded via "Ladda fler" fetchAllPages: add maxPages guard (default 500) to prevent infinite loop - If Arcim gateway returns hasMore:true indefinitely, the loop now exits after 500 pages instead of running forever Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a438c50e9c
commit
a7be1aab17
@@ -16,9 +16,9 @@ export default function BookkeepingPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">Bokföring</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Bokföring</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Verifikationer, kontoplan och manuella bokföringsorder
|
||||
Skapa verifikationer, hantera kontoplanen och bifoga underlag
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" asChild className="w-full sm:w-auto">
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function YearEndPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Årsbokslut</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Årsbokslut</h1>
|
||||
<Button variant="outline" asChild>
|
||||
<Link href="/bookkeeping">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
|
||||
@@ -84,8 +84,8 @@ export default function CustomerDetailPage({
|
||||
setCustomer(data)
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte hitta kunden',
|
||||
title: 'Kunde inte ladda kund',
|
||||
description: 'Kunden hittades inte.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
router.push('/customers')
|
||||
@@ -115,8 +115,8 @@ export default function CustomerDetailPage({
|
||||
fetchCustomer()
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte uppdatera kunden',
|
||||
title: 'Kunde inte uppdatera kund',
|
||||
description: 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -150,8 +150,8 @@ export default function CustomerDetailPage({
|
||||
router.push('/customers')
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte ta bort kunden',
|
||||
title: 'Kunde inte ta bort kund',
|
||||
description: 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -196,7 +196,7 @@ export default function CustomerDetailPage({
|
||||
<Icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{customer.name}</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{customer.name}</h1>
|
||||
<Badge variant="secondary">{customerTypeLabels[customer.customer_type]}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@@ -346,7 +346,7 @@ export default function CustomerDetailPage({
|
||||
{formatCurrency(invoice.total, invoice.currency)}
|
||||
</span>
|
||||
<Badge variant={invoice.payment_status === 'paid' ? 'success' : 'secondary'}>
|
||||
{invoice.payment_status === 'paid' ? 'Betald' : invoice.payment_status === 'overdue' ? 'Försenad' : 'Obetald'}
|
||||
{invoice.payment_status === 'paid' ? 'Betald' : invoice.payment_status === 'overdue' ? 'Förfallen' : 'Obetald'}
|
||||
</Badge>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -56,8 +56,8 @@ export default function CustomersPage() {
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte hämta kunder',
|
||||
title: 'Kunde inte ladda kunder',
|
||||
description: 'Kontrollera din anslutning och försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} else {
|
||||
@@ -83,8 +83,8 @@ export default function CustomersPage() {
|
||||
|
||||
if (!response.ok) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: result.error || 'Kunde inte skapa kund',
|
||||
title: 'Kunde inte skapa kund',
|
||||
description: result.error || 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} else {
|
||||
@@ -109,7 +109,7 @@ export default function CustomersPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">Kunder</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Kunder</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Hantera dina kunder och deras faktureringsuppgifter
|
||||
</p>
|
||||
|
||||
@@ -47,8 +47,8 @@ export default function DeadlinesPage() {
|
||||
setOverdueInvoices({ count: overdueCount, total: overdueTotal })
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte hamta data',
|
||||
title: 'Kunde inte ladda deadlines',
|
||||
description: 'Kontrollera din anslutning och försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -83,8 +83,8 @@ export default function DeadlinesPage() {
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte skapa deadline',
|
||||
title: 'Kunde inte skapa deadline',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
throw error
|
||||
@@ -111,8 +111,8 @@ export default function DeadlinesPage() {
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte uppdatera deadline',
|
||||
title: 'Kunde inte uppdatera status',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -139,8 +139,8 @@ export default function DeadlinesPage() {
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte uppdatera deadline',
|
||||
title: 'Kunde inte spara ändringar',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -164,8 +164,8 @@ export default function DeadlinesPage() {
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte ta bort deadline',
|
||||
title: 'Kunde inte ta bort deadline',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -175,7 +175,7 @@ export default function DeadlinesPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Deadlines</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Deadlines</h1>
|
||||
</div>
|
||||
{/* Overdue alert skeleton */}
|
||||
<div className="rounded-lg border p-4 animate-pulse">
|
||||
@@ -214,7 +214,7 @@ export default function DeadlinesPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Deadlines</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Deadlines</h1>
|
||||
</div>
|
||||
|
||||
{overdueInvoices.count > 0 && (
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function ExpenseDetailPage() {
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}`)
|
||||
const { data, error } = await res.json()
|
||||
if (error) {
|
||||
toast({ title: 'Fel', description: error, variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ladda utgift', description: error, variant: 'destructive' })
|
||||
} else {
|
||||
setInvoice(data)
|
||||
setPayAmount(String(data.remaining_amount))
|
||||
@@ -76,7 +76,7 @@ export default function ExpenseDetailPage() {
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}/approve`, { method: 'POST' })
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error, variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte godkänna', description: result.error, variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Godkänd', description: 'Utgiften har godkänts' })
|
||||
fetchInvoice()
|
||||
@@ -93,7 +93,7 @@ export default function ExpenseDetailPage() {
|
||||
})
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error, variant: 'destructive' })
|
||||
toast({ title: 'Betalning misslyckades', description: result.error, variant: 'destructive' })
|
||||
} else {
|
||||
toast({
|
||||
title: result.status === 'paid' ? 'Betald' : 'Delbetalning registrerad',
|
||||
@@ -117,7 +117,7 @@ export default function ExpenseDetailPage() {
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}/credit`, { method: 'POST' })
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error, variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte kreditera', description: result.error, variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Kreditfaktura registrerad' })
|
||||
fetchInvoice()
|
||||
@@ -136,7 +136,7 @@ export default function ExpenseDetailPage() {
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}`, { method: 'DELETE' })
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error, variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ta bort', description: result.error, variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Borttagen' })
|
||||
router.push('/expenses')
|
||||
@@ -177,7 +177,7 @@ export default function ExpenseDetailPage() {
|
||||
</Button>
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
|
||||
Utgift #{invoice.arrival_number}
|
||||
</h1>
|
||||
<Badge className={status.color}>
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function NewExpensePage() {
|
||||
|
||||
async function handleCreateSupplier() {
|
||||
if (!newSupplier.name.trim()) {
|
||||
toast({ title: 'Fel', description: 'Ange leverantörsnamn', variant: 'destructive' })
|
||||
toast({ title: 'Namn saknas', description: 'Ange ett namn för leverantören.', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
setIsCreatingSupplier(true)
|
||||
@@ -231,11 +231,11 @@ export default function NewExpensePage() {
|
||||
|
||||
function onSubmit(data: FormData) {
|
||||
if (!data.supplier_id) {
|
||||
toast({ title: 'Fel', description: 'Välj en leverantör', variant: 'destructive' })
|
||||
toast({ title: 'Leverantör saknas', description: 'Välj eller skapa en leverantör.', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
if (!data.supplier_invoice_number) {
|
||||
toast({ title: 'Fel', description: 'Ange fakturanummer', variant: 'destructive' })
|
||||
toast({ title: 'Fakturanummer saknas', description: 'Ange leverantörens fakturanummer.', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ export default function NewExpensePage() {
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Ny utgift</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Ny utgift</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Registrera en inkommande faktura
|
||||
</p>
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function ExpensesPage() {
|
||||
.order('due_date', { ascending: true })
|
||||
|
||||
if (error) {
|
||||
toast({ title: 'Fel', description: 'Kunde inte hämta utgifter', variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ladda utgifter', description: 'Kontrollera din anslutning och försök igen.', variant: 'destructive' })
|
||||
} else {
|
||||
setInvoices(data || [])
|
||||
}
|
||||
@@ -134,7 +134,7 @@ export default function ExpensesPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Att betala</p>
|
||||
<p className="text-2xl font-bold tabular-nums">{formatCurrency(stats.unpaidAmount)}</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{formatCurrency(stats.unpaidAmount)}</p>
|
||||
<p className="text-xs text-muted-foreground">{stats.unpaidCount} utgifter</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,13 +150,13 @@ export default function ExpensesPage() {
|
||||
{stats.overdueCount > 0 ? (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">Förfallet</p>
|
||||
<p className="text-2xl font-bold tabular-nums text-destructive">{formatCurrency(stats.overdueAmount)}</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums text-destructive">{formatCurrency(stats.overdueAmount)}</p>
|
||||
<p className="text-xs text-muted-foreground">{stats.overdueCount} utgifter</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">Totalt antal</p>
|
||||
<p className="text-2xl font-bold tabular-nums">{invoices.length}</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{invoices.length}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -438,7 +438,7 @@ function SIEImportWizard() {
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: data.error || 'Kunde inte skapa konton', variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte skapa konton', description: data.error || 'Försök igen.', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -463,7 +463,7 @@ function SIEImportWizard() {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toast({ title: 'Fel', description: err instanceof Error ? err.message : 'Kunde inte skapa konton', variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte skapa konton', description: err instanceof Error ? err.message : 'Försök igen.', variant: 'destructive' })
|
||||
} finally {
|
||||
setIsCreatingAccounts(false)
|
||||
}
|
||||
@@ -601,8 +601,8 @@ function PSD2ConnectWizard() {
|
||||
window.location.href = data.authorization_url
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte ansluta bank',
|
||||
title: 'Kunde inte ansluta bank',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
setIsConnecting(false)
|
||||
@@ -634,8 +634,8 @@ function PSD2ConnectWizard() {
|
||||
fetchConnections()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Synkronisering misslyckades',
|
||||
title: 'Synkronisering misslyckades',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -671,8 +671,8 @@ function PSD2ConnectWizard() {
|
||||
fetchConnections()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte koppla bort bank',
|
||||
title: 'Kunde inte koppla bort bank',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -780,7 +780,7 @@ export default function ImportPage() {
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Importera</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Importera</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Importera banktransaktioner eller bokföringsdata till ditt företag
|
||||
</p>
|
||||
|
||||
@@ -51,8 +51,8 @@ export default function CreateCreditNotePage({ params }: { params: Promise<{ id:
|
||||
|
||||
if (error || !data) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte hämta faktura',
|
||||
title: 'Kunde inte ladda faktura',
|
||||
description: 'Fakturan hittades inte.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
router.push('/invoices')
|
||||
@@ -120,8 +120,8 @@ export default function CreateCreditNotePage({ params }: { params: Promise<{ id:
|
||||
router.push(`/invoices/${creditNote.id}`)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte skapa kreditfaktura',
|
||||
title: 'Kunde inte skapa kreditfaktura',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -151,7 +151,7 @@ export default function CreateCreditNotePage({ params }: { params: Promise<{ id:
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Skapa kreditfaktura</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Skapa kreditfaktura</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Krediterar faktura {invoice.invoice_number}
|
||||
</p>
|
||||
|
||||
@@ -90,8 +90,8 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
|
||||
if (error || !data) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte hämta faktura',
|
||||
title: 'Kunde inte ladda faktura',
|
||||
description: 'Fakturan hittades inte.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
router.push('/invoices')
|
||||
@@ -211,8 +211,8 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
fetchInvoice()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte uppdatera status',
|
||||
title: 'Statusuppdatering misslyckades',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -255,8 +255,8 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
fetchInvoice()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte skicka fakturan',
|
||||
title: 'Kunde inte skicka faktura',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -287,8 +287,8 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
router.push(`/invoices/${data.data.id}`)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte konvertera',
|
||||
title: 'Konvertering misslyckades',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -324,8 +324,8 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
})
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte ladda ner PDF',
|
||||
title: 'Kunde inte ladda ner PDF',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -363,12 +363,12 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
</Button>
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">{invoice.invoice_number}</h1>
|
||||
<h1 className="font-display text-2xl sm:text-3xl font-medium tracking-tight">{invoice.invoice_number}</h1>
|
||||
{isProforma && (
|
||||
<Badge variant="secondary" className="bg-blue-100 text-blue-700">Proforma</Badge>
|
||||
<Badge variant="secondary" className="bg-primary/10 text-primary">Proforma</Badge>
|
||||
)}
|
||||
{isDeliveryNote && (
|
||||
<Badge variant="secondary" className="bg-emerald-100 text-emerald-700">Följesedel</Badge>
|
||||
<Badge variant="secondary" className="bg-success/10 text-success">Följesedel</Badge>
|
||||
)}
|
||||
<Badge variant={status.variant as 'default' | 'secondary' | 'destructive'}>
|
||||
<StatusIcon className="mr-1 h-3 w-3" />
|
||||
|
||||
@@ -168,8 +168,8 @@ export default function NewInvoicePage() {
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte hämta kunder',
|
||||
title: 'Kunde inte ladda kunder',
|
||||
description: 'Kontrollera din anslutning och försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} else {
|
||||
@@ -191,8 +191,8 @@ export default function NewInvoicePage() {
|
||||
|
||||
if (!response.ok) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: result.error || 'Kunde inte skapa kund',
|
||||
title: 'Kunde inte skapa kund',
|
||||
description: result.error || 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} else {
|
||||
@@ -370,7 +370,7 @@ export default function NewInvoicePage() {
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">
|
||||
{watchDocumentType === 'proforma' ? 'Ny proformafaktura' : watchDocumentType === 'delivery_note' ? 'Ny följesedel' : 'Ny faktura'}
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
|
||||
@@ -11,17 +11,18 @@ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { PageHeader } from '@/components/ui/page-header'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import { formatCurrency, formatDate } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Plus, Search, Receipt, FileText, Send, CheckCircle, Clock, XCircle, ReceiptText, FileQuestion, Truck } from 'lucide-react'
|
||||
import { EmptyInvoices } from '@/components/ui/empty-state'
|
||||
import type { Invoice, InvoiceStatus } from '@/types'
|
||||
|
||||
const statusConfig: Record<InvoiceStatus, { label: string; variant: 'default' | 'secondary' | 'success' | 'warning' | 'destructive'; icon: React.ElementType; borderColor: string }> = {
|
||||
draft: { label: 'Utkast', variant: 'secondary', icon: FileText, borderColor: 'border-l-muted-foreground/30' },
|
||||
sent: { label: 'Skickad', variant: 'default', icon: Send, borderColor: 'border-l-warning' },
|
||||
paid: { label: 'Betald', variant: 'success', icon: CheckCircle, borderColor: 'border-l-success' },
|
||||
overdue: { label: 'Förfallen', variant: 'destructive', icon: Clock, borderColor: 'border-l-destructive' },
|
||||
cancelled: { label: 'Makulerad', variant: 'secondary', icon: XCircle, borderColor: 'border-l-muted-foreground/30' },
|
||||
credited: { label: 'Krediterad', variant: 'secondary', icon: XCircle, borderColor: 'border-l-muted-foreground/30' },
|
||||
draft: { label: 'Utkast', variant: 'secondary', icon: FileText, borderColor: 'border-muted-foreground/30' },
|
||||
sent: { label: 'Skickad', variant: 'default', icon: Send, borderColor: 'border-warning/50' },
|
||||
paid: { label: 'Betald', variant: 'success', icon: CheckCircle, borderColor: 'border-success/50' },
|
||||
overdue: { label: 'Förfallen', variant: 'destructive', icon: Clock, borderColor: 'border-destructive/50' },
|
||||
cancelled: { label: 'Makulerad', variant: 'secondary', icon: XCircle, borderColor: 'border-muted-foreground/30' },
|
||||
credited: { label: 'Krediterad', variant: 'secondary', icon: XCircle, borderColor: 'border-muted-foreground/30' },
|
||||
}
|
||||
|
||||
function getRelativeTimeLabel(dueDateStr: string, status: InvoiceStatus): { text: string; color: string } | null {
|
||||
@@ -62,8 +63,8 @@ export default function InvoicesPage() {
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte hämta fakturor',
|
||||
title: 'Kunde inte ladda fakturor',
|
||||
description: 'Kontrollera din anslutning och försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} else {
|
||||
@@ -106,7 +107,7 @@ export default function InvoicesPage() {
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Fakturor"
|
||||
description="Skapa och hantera dina fakturor"
|
||||
description="Skicka fakturor, följ betalningar och skapa kreditnotor"
|
||||
action={
|
||||
<Link href="/invoices/new">
|
||||
<Button>
|
||||
@@ -145,7 +146,7 @@ export default function InvoicesPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Totalt antal</p>
|
||||
<p className="text-2xl font-bold tabular-nums">{invoices.length}</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{invoices.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -159,7 +160,7 @@ export default function InvoicesPage() {
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Obetalda</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-2xl font-bold tabular-nums">{stats.unpaid}</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{stats.unpaid}</p>
|
||||
{stats.overdue > 0 && (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
{stats.overdue} förfallna
|
||||
@@ -178,7 +179,7 @@ export default function InvoicesPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Att få in</p>
|
||||
<p className="text-2xl font-bold tabular-nums">{formatCurrency(stats.unpaidAmount)}</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{formatCurrency(stats.unpaidAmount)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -262,11 +263,13 @@ export default function InvoicesPage() {
|
||||
const isDeliveryNote = docType === 'delivery_note'
|
||||
const StatusIcon = isCreditNote ? ReceiptText : isProforma ? FileQuestion : isDeliveryNote ? Truck : status.icon
|
||||
const relativeTime = invoice.due_date ? getRelativeTimeLabel(invoice.due_date, invoice.status) : null
|
||||
const borderClass = isCreditNote ? 'border-l-4 border-l-destructive/50' : isProforma ? 'border-l-4 border-l-blue-400' : isDeliveryNote ? 'border-l-4 border-l-emerald-400' : `border-l-4 ${status.borderColor}`
|
||||
|
||||
return (
|
||||
<Link key={invoice.id} href={`/invoices/${invoice.id}`}>
|
||||
<Card className={`hover:border-primary/50 transition-colors cursor-pointer ${borderClass} ${invoice.status === 'overdue' ? 'ring-1 ring-destructive/20' : ''}`}>
|
||||
<Card className={cn(
|
||||
'hover:border-primary/50 transition-colors cursor-pointer',
|
||||
isCreditNote ? 'border-destructive/30' : isProforma ? 'border-primary/30' : isDeliveryNote ? 'border-success/30' : status.borderColor,
|
||||
invoice.status === 'overdue' && 'ring-1 ring-destructive/20'
|
||||
)}>
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-start gap-3 sm:gap-4 sm:items-center">
|
||||
<div className={`h-10 w-10 rounded-lg flex items-center justify-center shrink-0 ${isCreditNote ? 'bg-destructive/10' : 'bg-muted'}`}>
|
||||
@@ -289,12 +292,12 @@ export default function InvoicesPage() {
|
||||
</Badge>
|
||||
)}
|
||||
{isProforma && (
|
||||
<Badge variant="secondary" className="text-xs bg-blue-100 text-blue-700">
|
||||
<Badge variant="secondary" className="text-xs bg-primary/10 text-primary">
|
||||
Proforma
|
||||
</Badge>
|
||||
)}
|
||||
{isDeliveryNote && (
|
||||
<Badge variant="secondary" className="text-xs bg-emerald-100 text-emerald-700">
|
||||
<Badge variant="secondary" className="text-xs bg-success/10 text-success">
|
||||
Följesedel
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function ReceiptsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Kvitton</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Kvitton</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Hantera och granska dina kvitton
|
||||
</p>
|
||||
|
||||
@@ -69,9 +69,9 @@ export default function ReportsPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">Rapporter</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Rapporter</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Huvudbok, grundbok, kundreskontra, saldobalans, resultaträkning, balansräkning, momsdeklaration och mer
|
||||
Generera skattedeklarationer, resultaträkningar och exportera till Skatteverket
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1160,7 +1160,7 @@ function SupplierLedgerView({ periodId }: { periodId: string }) {
|
||||
<CardTitle className="text-sm text-muted-foreground">Totalt utestående</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">{formatAmount(ledger.total_outstanding)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{formatAmount(ledger.total_outstanding)} kr</p>
|
||||
<p className="text-xs text-muted-foreground">{ledger.unpaid_count} fakturor</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -1169,7 +1169,7 @@ function SupplierLedgerView({ periodId }: { periodId: string }) {
|
||||
<CardTitle className="text-sm text-muted-foreground">Ej förfallet</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold text-success">{formatAmount(ledger.total_current)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums text-success">{formatAmount(ledger.total_current)} kr</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -1177,7 +1177,7 @@ function SupplierLedgerView({ periodId }: { periodId: string }) {
|
||||
<CardTitle className="text-sm text-muted-foreground">Förfallet</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold text-destructive">{formatAmount(ledger.total_overdue)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums text-destructive">{formatAmount(ledger.total_overdue)} kr</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -1758,7 +1758,7 @@ function ARLedgerView({ periodId }: { periodId: string }) {
|
||||
<CardTitle className="text-sm text-muted-foreground">Totalt utestående</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">{formatAmount(ledger.total_outstanding)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{formatAmount(ledger.total_outstanding)} kr</p>
|
||||
<p className="text-xs text-muted-foreground">{ledger.unpaid_count} fakturor</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -1767,7 +1767,7 @@ function ARLedgerView({ periodId }: { periodId: string }) {
|
||||
<CardTitle className="text-sm text-muted-foreground">Ej förfallet</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold text-success">{formatAmount(ledger.total_current)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums text-success">{formatAmount(ledger.total_current)} kr</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -1775,7 +1775,7 @@ function ARLedgerView({ periodId }: { periodId: string }) {
|
||||
<CardTitle className="text-sm text-muted-foreground">Förfallet</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold text-destructive">{formatAmount(ledger.total_overdue)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums text-destructive">{formatAmount(ledger.total_overdue)} kr</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -196,8 +196,8 @@ export default function SettingsPage() {
|
||||
setSettings({ ...settings, ...updates } as typeof settings)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte spara inställningar',
|
||||
title: 'Kunde inte spara inställningar',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -229,8 +229,8 @@ export default function SettingsPage() {
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error instanceof Error ? error.message : 'Kunde inte radera kontot',
|
||||
title: 'Kunde inte radera kontot',
|
||||
description: error instanceof Error ? error.message : 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
setIsDeleting(false)
|
||||
@@ -284,7 +284,7 @@ export default function SettingsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Inställningar</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Inställningar</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Hantera dina företags- och kontoinställningar
|
||||
</p>
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}`)
|
||||
const { data, error } = await res.json()
|
||||
if (error) {
|
||||
toast({ title: 'Fel', description: error, variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ladda leverantörsfaktura', description: error, variant: 'destructive' })
|
||||
} else {
|
||||
setInvoice(data)
|
||||
setPayAmount(String(data.remaining_amount))
|
||||
@@ -72,7 +72,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}/approve`, { method: 'POST' })
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error, variant: 'destructive' })
|
||||
toast({ title: 'Godkännande misslyckades', description: result.error, variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Godkänd', description: 'Fakturan har godkänts' })
|
||||
fetchInvoice()
|
||||
@@ -89,7 +89,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
})
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error, variant: 'destructive' })
|
||||
toast({ title: 'Betalning misslyckades', description: result.error, variant: 'destructive' })
|
||||
} else {
|
||||
toast({
|
||||
title: result.status === 'paid' ? 'Betald' : 'Delbetalning registrerad',
|
||||
@@ -113,7 +113,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}/credit`, { method: 'POST' })
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error, variant: 'destructive' })
|
||||
toast({ title: 'Kreditering misslyckades', description: result.error, variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Kreditfaktura registrerad' })
|
||||
fetchInvoice()
|
||||
@@ -132,7 +132,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
const res = await fetch(`/api/supplier-invoices/${params.id}`, { method: 'DELETE' })
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error, variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ta bort faktura', description: result.error, variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Borttagen' })
|
||||
router.push('/supplier-invoices')
|
||||
@@ -172,7 +172,7 @@ export default function SupplierInvoiceDetailPage() {
|
||||
</Button>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">
|
||||
<h1 className="font-display text-2xl sm:text-3xl font-medium tracking-tight">
|
||||
Ankomst #{invoice.arrival_number}
|
||||
</h1>
|
||||
<Badge className={statusColors[invoice.status] || ''}>
|
||||
|
||||
@@ -165,11 +165,11 @@ export default function NewSupplierInvoicePage() {
|
||||
|
||||
function onSubmit(data: FormData) {
|
||||
if (!data.supplier_id) {
|
||||
toast({ title: 'Fel', description: 'Välj en leverantör', variant: 'destructive' })
|
||||
toast({ title: 'Leverantör saknas', description: 'Välj en leverantör innan du fortsätter.', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
if (!data.supplier_invoice_number) {
|
||||
toast({ title: 'Fel', description: 'Ange fakturanummer', variant: 'destructive' })
|
||||
toast({ title: 'Fakturanummer saknas', description: 'Ange leverantörens fakturanummer.', variant: 'destructive' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ export default function NewSupplierInvoicePage() {
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Registrera leverantörsfaktura</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Registrera leverantörsfaktura</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Registrera en inkommande faktura (uppfyller BFL verifikationskrav)
|
||||
</p>
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function SupplierInvoicesPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">Leverantörsfakturor</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Leverantörsfakturor</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Registrera och hantera inkommande fakturor
|
||||
</p>
|
||||
@@ -112,7 +112,7 @@ export default function SupplierInvoicesPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">{formatAmount(totalUnpaid)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{formatAmount(totalUnpaid)} kr</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{invoices.filter((i) => !['paid', 'credited'].includes(i.status)).length} fakturor
|
||||
</p>
|
||||
@@ -126,7 +126,7 @@ export default function SupplierInvoicesPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold text-destructive">{formatAmount(overdueAmount)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums text-destructive">{formatAmount(overdueAmount)} kr</p>
|
||||
<p className="text-xs text-muted-foreground">{overdueCount} fakturor</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -138,7 +138,7 @@ export default function SupplierInvoicesPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">{invoices.length}</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{invoices.length}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function SupplierDetailPage() {
|
||||
const res = await fetch(`/api/suppliers/${params.id}`)
|
||||
const { data, error } = await res.json()
|
||||
if (error) {
|
||||
toast({ title: 'Fel', description: error, variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ladda leverantör', description: error, variant: 'destructive' })
|
||||
} else {
|
||||
setSupplier(data)
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export default function SupplierDetailPage() {
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
const fieldErrors = result.errors?.map((e: { field: string; message: string }) => `${e.field}: ${e.message}`).join(', ')
|
||||
toast({ title: 'Fel', description: fieldErrors || result.error || 'Kunde inte uppdatera leverantör', variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte uppdatera leverantör', description: fieldErrors || result.error || 'Försök igen.', variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Sparat', description: 'Leverantören har uppdaterats' })
|
||||
setSupplier({ ...result.data, stats: supplier?.stats })
|
||||
@@ -90,7 +90,7 @@ export default function SupplierDetailPage() {
|
||||
const res = await fetch(`/api/suppliers/${params.id}`, { method: 'DELETE' })
|
||||
const result = await res.json()
|
||||
if (!res.ok) {
|
||||
toast({ title: 'Fel', description: result.error, variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ta bort leverantör', description: result.error, variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Borttagen', description: 'Leverantören har tagits bort' })
|
||||
router.push('/suppliers')
|
||||
@@ -145,7 +145,7 @@ export default function SupplierDetailPage() {
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">{supplier.name}</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">{supplier.name}</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{supplierTypeLabels[supplier.supplier_type]}
|
||||
{supplier.org_number && ` | Org.nr: ${supplier.org_number}`}
|
||||
@@ -170,7 +170,7 @@ export default function SupplierDetailPage() {
|
||||
<CardTitle className="text-sm text-muted-foreground">Utestående</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">{formatAmount(supplier.stats?.total_outstanding || 0)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{formatAmount(supplier.stats?.total_outstanding || 0)} kr</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -178,7 +178,7 @@ export default function SupplierDetailPage() {
|
||||
<CardTitle className="text-sm text-muted-foreground">Totalt betalt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">{formatAmount(supplier.stats?.total_paid || 0)} kr</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{formatAmount(supplier.stats?.total_paid || 0)} kr</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -186,7 +186,7 @@ export default function SupplierDetailPage() {
|
||||
<CardTitle className="text-sm text-muted-foreground">Antal fakturor</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold">{supplier.stats?.invoice_count || 0}</p>
|
||||
<p className="font-display text-2xl font-medium tabular-nums">{supplier.stats?.invoice_count || 0}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -43,8 +43,8 @@ export default function SuppliersPage() {
|
||||
|
||||
if (error) {
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: 'Kunde inte hämta leverantörer',
|
||||
title: 'Kunde inte ladda leverantörer',
|
||||
description: 'Kontrollera din anslutning och försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} else {
|
||||
@@ -71,8 +71,8 @@ export default function SuppliersPage() {
|
||||
if (!response.ok) {
|
||||
const fieldErrors = result.errors?.map((e: { field: string; message: string }) => `${e.field}: ${e.message}`).join(', ')
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: fieldErrors || result.error || 'Kunde inte skapa leverantör',
|
||||
title: 'Kunde inte skapa leverantör',
|
||||
description: fieldErrors || result.error || 'Försök igen.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
} else {
|
||||
@@ -97,7 +97,7 @@ export default function SuppliersPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">Leverantörer</h1>
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight">Leverantörer</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Hantera dina leverantörer och deras betalningsuppgifter
|
||||
</p>
|
||||
|
||||
@@ -76,6 +76,10 @@ export default function TransactionsPage() {
|
||||
// Entity type for tooltip context
|
||||
const [entityType, setEntityType] = useState<string>('enskild_firma')
|
||||
|
||||
// Pagination
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
|
||||
// Set of transaction IDs that are animating out (just categorized)
|
||||
const [exitingIds, setExitingIds] = useState<Set<string>>(new Set())
|
||||
|
||||
@@ -93,15 +97,18 @@ export default function TransactionsPage() {
|
||||
})
|
||||
const transactionsWithMatches = transactions.filter((t) => t.potential_invoice && !t.invoice_id)
|
||||
|
||||
const PAGE_SIZE = 200
|
||||
|
||||
async function fetchTransactions() {
|
||||
setIsLoading(true)
|
||||
const { data: txData, error: txError } = await supabase
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.order('date', { ascending: false })
|
||||
.limit(PAGE_SIZE)
|
||||
|
||||
if (txError) {
|
||||
toast({ title: 'Fel', description: 'Kunde inte hämta transaktioner', variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ladda transaktioner', description: 'Kontrollera din anslutning och försök igen.', variant: 'destructive' })
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
@@ -149,9 +156,71 @@ export default function TransactionsPage() {
|
||||
}))
|
||||
|
||||
setTransactions(transactionsWithInvoices)
|
||||
setHasMore((txData || []).length >= PAGE_SIZE)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
async function loadMoreTransactions() {
|
||||
setIsLoadingMore(true)
|
||||
const offset = transactions.length
|
||||
const { data: txData, error: txError } = await supabase
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.order('date', { ascending: false })
|
||||
.range(offset, offset + PAGE_SIZE - 1)
|
||||
|
||||
if (txError || !txData) {
|
||||
setIsLoadingMore(false)
|
||||
return
|
||||
}
|
||||
|
||||
setHasMore(txData.length >= PAGE_SIZE)
|
||||
|
||||
const potentialInvoiceIds = txData
|
||||
.filter((t) => t.potential_invoice_id)
|
||||
.map((t) => t.potential_invoice_id)
|
||||
|
||||
const unbookedTxIds = txData
|
||||
.filter((t) => !t.journal_entry_id && t.is_business === null)
|
||||
.map((t) => t.id)
|
||||
|
||||
const [invoiceResult, inboxResult] = await Promise.all([
|
||||
potentialInvoiceIds.length > 0
|
||||
? supabase.from('invoices').select('*, customer:customers(*)').in('id', potentialInvoiceIds)
|
||||
: Promise.resolve({ data: null }),
|
||||
unbookedTxIds.length > 0
|
||||
? supabase.from('invoice_inbox_items').select('*').in('matched_transaction_id', unbookedTxIds).in('status', ['ready', 'processing'])
|
||||
: Promise.resolve({ data: null }),
|
||||
])
|
||||
|
||||
let invoiceMap: Record<string, Invoice & { customer?: Customer }> = {}
|
||||
if (invoiceResult.data) {
|
||||
invoiceMap = invoiceResult.data.reduce((acc, inv) => {
|
||||
acc[inv.id] = inv
|
||||
return acc
|
||||
}, {} as Record<string, Invoice & { customer?: Customer }>)
|
||||
}
|
||||
|
||||
let inboxItemMap: Record<string, InvoiceInboxItem> = {}
|
||||
if (inboxResult.data) {
|
||||
inboxItemMap = inboxResult.data.reduce((acc, item) => {
|
||||
if (item.matched_transaction_id) {
|
||||
acc[item.matched_transaction_id] = item as InvoiceInboxItem
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, InvoiceInboxItem>)
|
||||
}
|
||||
|
||||
const newTransactions: TransactionWithInvoice[] = txData.map((t) => ({
|
||||
...t,
|
||||
potential_invoice: t.potential_invoice_id ? invoiceMap[t.potential_invoice_id] : undefined,
|
||||
matched_inbox_item: inboxItemMap[t.id] || undefined,
|
||||
}))
|
||||
|
||||
setTransactions((prev) => [...prev, ...newTransactions])
|
||||
setIsLoadingMore(false)
|
||||
}
|
||||
|
||||
async function fetchCategorySuggestions(txIds: string[]) {
|
||||
if (txIds.length === 0) return
|
||||
setIsLoadingSuggestions(true)
|
||||
@@ -161,6 +230,7 @@ export default function TransactionsPage() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ transaction_ids: txIds }),
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to fetch suggestions')
|
||||
const data = await response.json()
|
||||
if (data.suggestions) {
|
||||
setCategorySuggestions(data.suggestions)
|
||||
@@ -227,7 +297,7 @@ export default function TransactionsPage() {
|
||||
|
||||
const result = await response.json()
|
||||
if (!response.ok) {
|
||||
toast({ title: 'Fel', description: result.error || 'Kunde inte uppdatera transaktion', variant: 'destructive' })
|
||||
toast({ title: 'Kategorisering misslyckades', description: result.error || 'Försök igen.', variant: 'destructive' })
|
||||
setProcessingId(null)
|
||||
return null
|
||||
}
|
||||
@@ -254,10 +324,10 @@ export default function TransactionsPage() {
|
||||
toast({ title: 'Ångrad', description: 'Kategorisering har ångrats' })
|
||||
} else {
|
||||
const errData = await undoRes.json()
|
||||
toast({ title: 'Kunde inte ångra', description: errData.error || 'Något gick fel', variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ångra', description: errData.error || 'Kategoriseringen kunde inte ångras. Försök igen.', variant: 'destructive' })
|
||||
}
|
||||
} catch {
|
||||
toast({ title: 'Kunde inte ångra', description: 'Något gick fel', variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte ångra', description: 'Kategoriseringen kunde inte ångras. Försök igen.', variant: 'destructive' })
|
||||
}
|
||||
}}>
|
||||
Ångra
|
||||
@@ -290,7 +360,7 @@ export default function TransactionsPage() {
|
||||
|
||||
return result.journal_entry_id || null
|
||||
} catch {
|
||||
toast({ title: 'Fel', description: 'Något gick fel vid bokföring', variant: 'destructive' })
|
||||
toast({ title: 'Bokföring misslyckades', description: 'Transaktionen kunde inte bokföras. Försök igen.', variant: 'destructive' })
|
||||
setProcessingId(null)
|
||||
return null
|
||||
}
|
||||
@@ -312,7 +382,7 @@ export default function TransactionsPage() {
|
||||
})
|
||||
const result = await response.json()
|
||||
if (!response.ok) {
|
||||
toast({ title: 'Fel', description: result.error || 'Kunde inte matcha faktura', variant: 'destructive' })
|
||||
toast({ title: 'Fakturamatchning misslyckades', description: result.error || 'Försök igen.', variant: 'destructive' })
|
||||
setIsConfirmingMatch(false)
|
||||
return
|
||||
}
|
||||
@@ -350,7 +420,7 @@ export default function TransactionsPage() {
|
||||
setIsConfirmingMatch(false)
|
||||
}, 350)
|
||||
} catch {
|
||||
toast({ title: 'Fel', description: 'Något gick fel vid matchning', variant: 'destructive' })
|
||||
toast({ title: 'Matchning misslyckades', description: 'Transaktionen kunde inte matchas med fakturan. Försök igen.', variant: 'destructive' })
|
||||
setIsConfirmingMatch(false)
|
||||
}
|
||||
}
|
||||
@@ -364,7 +434,7 @@ export default function TransactionsPage() {
|
||||
})
|
||||
const result = await response.json()
|
||||
if (!response.ok) {
|
||||
toast({ title: 'Fel', description: result.error || 'Kunde inte matcha faktura', variant: 'destructive' })
|
||||
toast({ title: 'Fakturamatchning misslyckades', description: result.error || 'Försök igen.', variant: 'destructive' })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -390,7 +460,7 @@ export default function TransactionsPage() {
|
||||
toast({ title: 'Faktura matchad', description: `Faktura ${invoiceNumber} markerad som betald` })
|
||||
return true
|
||||
} catch {
|
||||
toast({ title: 'Fel', description: 'Något gick fel vid matchning', variant: 'destructive' })
|
||||
toast({ title: 'Matchning misslyckades', description: 'Transaktionen kunde inte matchas med fakturan. Försök igen.', variant: 'destructive' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -399,7 +469,7 @@ export default function TransactionsPage() {
|
||||
setIsCreating(true)
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) {
|
||||
toast({ title: 'Fel', description: 'Du måste vara inloggad', variant: 'destructive' })
|
||||
toast({ title: 'Inloggning krävs', description: 'Du måste vara inloggad för att lägga till transaktioner.', variant: 'destructive' })
|
||||
setIsCreating(false)
|
||||
return
|
||||
}
|
||||
@@ -420,7 +490,7 @@ export default function TransactionsPage() {
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
toast({ title: 'Fel', description: error.message, variant: 'destructive' })
|
||||
toast({ title: 'Kunde inte skapa transaktion', description: error.message, variant: 'destructive' })
|
||||
} else {
|
||||
toast({ title: 'Transaktion tillagd', description: `${data.description} har lagts till` })
|
||||
setTransactions([transaction, ...transactions])
|
||||
@@ -719,23 +789,39 @@ export default function TransactionsPage() {
|
||||
transactions={transactions}
|
||||
onOpenMatchDialog={openMatchDialog}
|
||||
onOpenCategoryDialog={openCategoryDialog}
|
||||
hasMore={hasMore}
|
||||
isLoadingMore={isLoadingMore}
|
||||
onLoadMore={loadMoreTransactions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Batch mode floating action bar */}
|
||||
{isBatchMode && selectedIds.size > 0 && (
|
||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-background border rounded-xl shadow-lg px-4 py-3">
|
||||
<Badge variant="secondary">{selectedIds.size} valda</Badge>
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedIds(new Set())}>
|
||||
<X className="mr-1 h-3 w-3" />
|
||||
Avmarkera
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleBatchMarkPrivate}>
|
||||
Markera som privat
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => setShowBatchSelector(true)}>
|
||||
Bokför {selectedIds.size} st
|
||||
</Button>
|
||||
<div className="fixed bottom-20 md:bottom-6 left-1/2 -translate-x-1/2 z-40 flex items-center gap-3 bg-background border rounded-xl shadow-lg px-4 py-3">
|
||||
{batchProgress ? (
|
||||
<>
|
||||
<Badge variant="secondary">
|
||||
{batchProgress.done}/{batchProgress.total}
|
||||
</Badge>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Bokför {batchProgress.done} av {batchProgress.total}...
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Badge variant="secondary">{selectedIds.size} valda</Badge>
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedIds(new Set())}>
|
||||
<X className="mr-1 h-3 w-3" />
|
||||
Avmarkera
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleBatchMarkPrivate}>
|
||||
Markera som privat
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => setShowBatchSelector(true)}>
|
||||
Bokför {selectedIds.size} st
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -114,8 +114,7 @@ function OnboardingPageContent() {
|
||||
.upsert({ ...settingsToSave, user_id: user.id }, { onConflict: 'user_id' })
|
||||
|
||||
if (error) {
|
||||
console.error('Error saving settings:', JSON.stringify(error), 'code:', error.code, 'message:', error.message, 'details:', error.details)
|
||||
console.error('Payload was:', JSON.stringify({ ...settingsToSave, user_id: user.id }))
|
||||
// Error details available in Sentry via toast context
|
||||
toast({
|
||||
title: 'Fel',
|
||||
description: error.message || 'Kunde inte spara. Försök igen.',
|
||||
@@ -170,8 +169,8 @@ function OnboardingPageContent() {
|
||||
p_entity_type: stepData.entity_type,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to seed chart of accounts:', err)
|
||||
} catch {
|
||||
// Non-critical — chart of accounts can be seeded later
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +422,7 @@ function OnboardingPageContent() {
|
||||
background: 'radial-gradient(ellipse at 30% -20%, rgba(255,255,255,0.04) 0%, transparent 50%)',
|
||||
}}
|
||||
/>
|
||||
<span className="absolute -bottom-4 right-4 md:right-10 text-[120px] md:text-[160px] font-serif font-bold text-white/[0.02] leading-none select-none">
|
||||
<span className="absolute -bottom-4 right-4 md:right-10 text-[120px] md:text-[160px] font-display font-bold text-white/[0.02] leading-none select-none">
|
||||
{String(currentStep).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
@@ -439,7 +438,7 @@ function OnboardingPageContent() {
|
||||
height={30}
|
||||
className="invert opacity-90"
|
||||
/>
|
||||
<span className="font-serif text-base tracking-tight">gnubok</span>
|
||||
<span className="font-display text-base tracking-tight">gnubok</span>
|
||||
</div>
|
||||
{/* Step indicator — inline with logo row */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -465,7 +464,7 @@ function OnboardingPageContent() {
|
||||
|
||||
{/* Step title — compact */}
|
||||
<div key={`title-${currentStep}`} className="animate-fade-in">
|
||||
<h1 className="font-serif text-2xl md:text-3xl font-medium tracking-tight leading-[1.1]">
|
||||
<h1 className="font-display text-2xl md:text-3xl font-medium tracking-tight leading-[1.1]">
|
||||
{stepInfo.title}
|
||||
</h1>
|
||||
<p className="text-white/40 mt-1.5 text-sm max-w-sm leading-relaxed">
|
||||
|
||||
+21
-3
@@ -9,10 +9,10 @@
|
||||
--background: 0 0% 97%;
|
||||
--foreground: 0 0% 9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card: 0 0% 99%;
|
||||
--card-foreground: 0 0% 9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover: 0 0% 99%;
|
||||
--popover-foreground: 0 0% 9%;
|
||||
|
||||
--primary: 0 0% 15%;
|
||||
@@ -22,7 +22,7 @@
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
|
||||
--muted: 0 0% 92%;
|
||||
--muted-foreground: 0 0% 45%;
|
||||
--muted-foreground: 0 0% 40%;
|
||||
|
||||
--accent: 0 0% 92%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
@@ -337,3 +337,21 @@ input[type="number"] {
|
||||
background: hsl(var(--primary) / 0.3);
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Respect reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
.stagger-enter > * {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hover-lift:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user