diff --git a/DECISIONS.md b/DECISIONS.md index c8035172..3bf56706 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -952,3 +952,5 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-13] PR #1598, compliance findings closed with the rollout after the Swedish accounting review escalated them from follow-up to fix-with-rollout: (a) runReconciliation's >= 0.9 auto-apply now writes 'matched' to payment_match_log (behandlingshistorik, BFNAR 2013:2 kap 8); (b) the three match-route storno-conflict branches no longer storno-reverse a reconciliation-linked verifikat: a reconciliation link points at an independent verifikat that may evidence other affarshandelser, and a wholesale reversal is an over-broad rattelse (BFL 5 kap 5 §). The detach is DEFERRED (round 2, CodeRabbit): nothing is persisted up front; the final transaction update overwrites the pointer and clears reconciliation_method in the same write, so a failure anywhere in the match flow leaves the existing link intact, and the release is logged as 'unmatched' after the commit. [2026-08-13] PR #1598, CodeRabbit findings: confirm-suggestions maxDuration 300; lookbackTouched on the migrator nudge buttons; persistSuggestions on main's post-backfill sweep; sie_sweep stamp errors logged; sandbox keeps the CSV CTA (file import works there); payment_match_log CHECK swap now NOT VALID + VALIDATE (no table-scan under ACCESS EXCLUSIVE); every logMatchEvent call awaited (serverless can freeze unawaited work). [2026-08-13] Historical audit gap quantified on prod (read-only): 762 manual-method links across 52 companies since 2026-03-23 have no payment_match_log row (upper bound: includes linked_to_existing_voucher drops AND older unlogged manual paths). Not backfillable (the inserts never landed); the links themselves are intact on transactions. Recorded here as the explicit ops note the compliance review asked for. +[2026-08-13] Startkort empty states use inline gradient scrims over their strata images: this is imagery treatment inside a hero surface, not card chrome, so the "no bg-gradient on cards" rule deliberately does not apply there (and nowhere else). +[2026-08-13] Startkort webp assets (public/startkort/) are rendered outputs from the strata-engine in the CRM workspace, with per-file sources and flags recorded in components/dashboard/startkort-assets.ts; regenerate there, never edit the webp files by hand. diff --git a/app/(dashboard)/bookkeeping/page.tsx b/app/(dashboard)/bookkeeping/page.tsx index 76d3a61e..554eecf3 100644 --- a/app/(dashboard)/bookkeeping/page.tsx +++ b/app/(dashboard)/bookkeeping/page.tsx @@ -5,6 +5,7 @@ import dynamic from 'next/dynamic' import { useRouter, useSearchParams } from 'next/navigation' import { useTranslations } from 'next-intl' import JournalEntryList from '@/components/bookkeeping/JournalEntryList' +import { StartCard } from '@/components/dashboard/StartCard' import { type FormLine } from '@/components/bookkeeping/JournalEntryForm' import type { CopyPrefill } from '@/components/bookkeeping/NewJournalEntryDialog' import { DialogLoadingSkeleton } from '@/components/ui/dialog-loading-skeleton' @@ -54,6 +55,7 @@ export default function BookkeepingPage() { const [isLoadingCopy, setIsLoadingCopy] = useState(false) const [nextVoucher, setNextVoucher] = useState(null) const t = useTranslations('bookkeeping') + const tStart = useTranslations('start_cards') const { openAgentSheet } = useAgentSheet() const { uiState, loaded: uiStateLoaded } = useUiState() @@ -186,7 +188,53 @@ export default function BookkeepingPage() { } /> - + + + {/* The split button's three create modes, laid out as cards so the + pristine page shows what the ledger can do instead of a bare table. */} +
+ {( + [ + ['mall', () => setShowTemplateDialog(true)], + [ + 'assistent', + () => openAgentSheet({ intentId: 'verifikation.draft', contextRef: 'verifikation:new' }), + ], + [ + 'tomt', + () => { + setCopyPrefill(null) + setShowNewEntry(true) + }, + ], + ] as const + ).map(([mode, onClick]) => ( + + ))} +
+ + } + /> {showTemplateDialog && ( ) : invoices.length === 0 ? ( - +
+ +
) : ( } diff --git a/app/(dashboard)/salary/page.tsx b/app/(dashboard)/salary/page.tsx index 04da9265..8ba15833 100644 --- a/app/(dashboard)/salary/page.tsx +++ b/app/(dashboard)/salary/page.tsx @@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Button } from '@/components/ui/button' import { EmptyState } from '@/components/ui/empty-state' +import { StartCard } from '@/components/dashboard/StartCard' import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table' import { HandCoins, Loader2, Plus, Users } from 'lucide-react' import { useToast } from '@/components/ui/use-toast' @@ -53,6 +54,7 @@ export default function SalaryPage() { const { toast } = useToast() const router = useRouter() const t = useTranslations('salary') + const tStart = useTranslations('start_cards') const load = useCallback(async () => { const [runsRes, empRes] = await Promise.all([ @@ -146,23 +148,37 @@ export default function SalaryPage() { {header} {runs.length === 0 && employees.length === 0 ? ( - + canWrite ? ( +
+ +
+ ) : ( + + ) ) : (
{runs.length === 0 ? ( - + canWrite ? ( +
+ +
+ ) : ( + + ) ) : (
diff --git a/app/(dashboard)/skattekonto/page.tsx b/app/(dashboard)/skattekonto/page.tsx index 7133ea55..fb446ad9 100644 --- a/app/(dashboard)/skattekonto/page.tsx +++ b/app/(dashboard)/skattekonto/page.tsx @@ -10,6 +10,7 @@ import { PageHeader } from '@/components/ui/page-header' import { HelpPopover } from '@/components/ui/help-popover' import { AttnLine } from '@/components/ui/attn-line' import { EmptyState } from '@/components/ui/empty-state' +import { StartCard } from '@/components/dashboard/StartCard' import { Skeleton } from '@/components/ui/skeleton' import { TH_CLASS, @@ -42,8 +43,6 @@ import { rowsNeedingInterestDate } from '@/lib/skatteverket/interest-period' import { AlertCircle, Copy, - ExternalLink, - Landmark, RefreshCw, } from 'lucide-react' import { useCapability } from '@/contexts/CompanyContext' @@ -89,6 +88,7 @@ interface MatchCandidate { export default function SkattekontoPage() { const { toast } = useToast() const t = useTranslations('skattekonto') + const tStart = useTranslations('start_cards') const hasSkvCapability = useCapability(CAPABILITY.skatteverket) const [showPayment, setShowPayment] = useState(false) const [saldo, setSaldo] = useState(null) @@ -374,18 +374,15 @@ export default function SkattekontoPage() { return (
- - - +
+ +
) } diff --git a/components/bookkeeping/JournalEntryList.tsx b/components/bookkeeping/JournalEntryList.tsx index e235c360..1beab2ee 100644 --- a/components/bookkeeping/JournalEntryList.tsx +++ b/components/bookkeeping/JournalEntryList.tsx @@ -1,6 +1,6 @@ 'use client' -import { Fragment, useState, useEffect, useCallback, useRef } from 'react' +import { Fragment, useState, useEffect, useCallback, useRef, type ReactNode } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' @@ -189,7 +189,7 @@ const PAGE_SIZE_VALUES = new Set(['20', '50', '100', 'all']) // Sentinel limit sent for "Alla". The route clamps this to its own MAX_LIMIT. const ALL_PAGE_SIZE = 100000 -export default function JournalEntryList() { +export default function JournalEntryList({ pristineSlot }: { pristineSlot?: ReactNode } = {}) { const router = useRouter() const { toast } = useToast() const { canWrite } = useCanWrite() @@ -932,6 +932,9 @@ export default function JournalEntryList() { // drafts view) must fall through to the main render below so the // Verifikat/Utkast toggle stays reachable. if (!loading && entries.length === 0 && !loadFailed && !hasActiveFilters && listMode === 'committed' && draftCount === 0) { + if (pristineSlot) { + return <>{pristineSlot} + } return ( void +} + +interface StartCardProps { + card: StartCardName + /** 'side-right': image on the right half. 'bleed-left': full-bleed image, + * text over a scrim on the left. 'center': full-bleed, centered text. */ + layout: 'side-right' | 'bleed-left' | 'center' + title: string + body: string + eyebrow?: string + primary: StartCardAction + secondary?: StartCardAction + /** Floating channel badges over the right half (Underlag). */ + floatIcons?: boolean + /** Tighter type for narrow containers (the inbox preview pane). */ + dense?: boolean + onDismiss?: () => void + dismissLabel?: string + className?: string +} + +function ActionButton({ action, kind }: { action: StartCardAction; kind: 'primary' | 'secondary' }) { + const styles = + kind === 'primary' + ? 'bg-white text-[#1A1410] hover:bg-white/90 active:bg-white/90 focus-visible:ring-white/60 focus-visible:ring-offset-transparent' + : 'border border-white/30 bg-transparent text-[#F7F4EC] hover:bg-white/10 active:bg-white/10 focus-visible:ring-white/60 focus-visible:ring-offset-transparent' + if (action.href) { + return ( + + ) + } + return ( + + ) +} + +// Channel badges for the Underlag card, Wispr-style: translucent circles over +// the image, one deliberately cropped by the card edge. Brand marks inline so +// the card stays self-contained (Gmail's M, WhatsApp's glyph). +const BADGE_STROKE = '#F4F1E8' + +function badgeSvg(paths: string) { + return ( + + ) +} + +const FLOAT_BADGES: Array<{ style: CSSProperties; icon: ReactNode }> = [ + { + style: { right: '34%', top: '8%', width: 38, height: 38 }, + icon: badgeSvg( + '', + ), + }, + { + style: { right: '6%', top: '14%', width: 46, height: 46 }, + icon: ( + + ), + }, + { + style: { right: '22%', top: '46%', width: 54, height: 54 }, + icon: ( + + ), + }, + { + style: { right: '5%', top: '64%', width: 42, height: 42 }, + icon: badgeSvg( + '', + ), + }, + { + style: { right: '26%', bottom: '-8%', width: 50, height: 50 }, + icon: badgeSvg( + '', + ), + }, +] + +export function StartCard({ + card, + layout, + title, + body, + eyebrow, + primary, + secondary, + floatIcons = false, + dense = false, + onDismiss, + dismissLabel, + className, +}: StartCardProps) { + const asset = START_CARDS[card] + const g = asset.groundRgb + + const scrim = + layout === 'center' + ? `radial-gradient(ellipse 90% 130% at 50% 55%, rgba(${g}, 0.7) 0%, rgba(${g}, 0.36) 55%, rgba(${g}, 0.06) 100%)` + : `linear-gradient(90deg, rgba(${g}, 0.92) 0%, rgba(${g}, 0.55) 45%, rgba(${g}, 0.08) 75%)` + + return ( +
+ {layout === 'side-right' ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+ ) : ( + <> + {/* eslint-disable-next-line @next/next/no-img-element */} + +
+ + )} + + {floatIcons && + FLOAT_BADGES.map((badge, i) => ( + + ))} + + {onDismiss && ( + + )} + +
+ {eyebrow && ( +
+ {eyebrow} +
+ )} +

+ {title} +

+

+ {body} +

+
+ + {secondary && } +
+
+
+ ) +} diff --git a/components/dashboard/startkort-assets.ts b/components/dashboard/startkort-assets.ts new file mode 100644 index 00000000..bf183b02 --- /dev/null +++ b/components/dashboard/startkort-assets.ts @@ -0,0 +1,70 @@ +// Strata renders for the start cards (public/startkort/*.webp). Generated +// deterministically by strata-engine in the CRM workspace (studio/strata): +// +// node stratify.mjs linen-clear --bg [flags] --name +// +// Per-file source + flags, so any card can be regenerated or re-grounded: +// venice.webp imports/venice-canal-painting/04-commons-giovanni-antonio-canal...jpg --bg #3A2118 +// sthlm.webp imports/stockholm-gamla-stan-riddarholmen-waterfront-skyline/05-web...jpg --bg #16303B +// geese.webp imports/byt-hero/02-commons-canadiangeeseflyinginvformation-jpg.jpg --bg #2E3C58 +// stopwatch.webp imports/konsult-hero/04-flickr-stopwatch.jpg (night-clear) --bg #322416 +// ledger.webp imports/ledger/01-commons-pittsboro-merchant-business-ledger-dpla-.jpg --bg #1F2B20 --tone 0.35 +// abacus.webp imports/abacus/04-commons-abacus-of-state-department-store-jpg.jpg (right unit crop) --bg #1B2136 --tone 0.5 +// +// The ground color is baked into the render (the gaps between bars carry it), +// so the card background and scrims below must stay in the same hue: change +// them together or the image edge shows a seam. Intrinsic dimensions declared +// up front so the gets width/height and avoids layout shift. + +export const START_CARDS = { + venice: { + src: '/startkort/venice.webp', + w: 2400, + h: 1466, + ground: '#3A2118', + groundRgb: '58, 33, 24', + objectPosition: 'center 42%', + }, + sthlm: { + src: '/startkort/sthlm.webp', + w: 2400, + h: 1500, + ground: '#16303B', + groundRgb: '22, 48, 59', + objectPosition: 'center 32%', + }, + geese: { + src: '/startkort/geese.webp', + w: 2400, + h: 1600, + ground: '#2E3C58', + groundRgb: '46, 60, 88', + objectPosition: '0 26%', + }, + stopwatch: { + src: '/startkort/stopwatch.webp', + w: 2400, + h: 1800, + ground: '#322416', + groundRgb: '50, 36, 22', + objectPosition: 'center 30%', + }, + ledger: { + src: '/startkort/ledger.webp', + w: 2400, + h: 3602, + ground: '#1F2B20', + groundRgb: '31, 43, 32', + objectPosition: 'center 30%', + }, + abacus: { + src: '/startkort/abacus.webp', + w: 2400, + h: 2176, + ground: '#1B2136', + groundRgb: '27, 33, 54', + objectPosition: 'center 35%', + }, +} as const + +export type StartCardName = keyof typeof START_CARDS diff --git a/components/extensions/general/InvoiceInboxWorkspace.tsx b/components/extensions/general/InvoiceInboxWorkspace.tsx index 0fd6713b..b8821a53 100644 --- a/components/extensions/general/InvoiceInboxWorkspace.tsx +++ b/components/extensions/general/InvoiceInboxWorkspace.tsx @@ -39,18 +39,16 @@ import { ExternalLink, FileQuestion, Search, - Circle, - X, ChevronDown, ChevronRight, Sparkles, - MessageCircle, Maximize2, } from 'lucide-react' import Link from 'next/link' import { cn, formatCurrency, formatDate, formatDateLong } from '@/lib/utils' import { QUIET_LINK_CLASS } from '@/components/ui/dry-table' import { GoogleMark, MicrosoftMark } from '@/components/ui/provider-marks' +import { StartCard } from '@/components/dashboard/StartCard' import EditKonteringDialog from '@/components/extensions/general/EditKonteringDialog' import InvoiceInboxSkeleton from '@/components/extensions/general/InvoiceInboxSkeleton' import { WhatsAppMark } from '@/components/extensions/general/WhatsAppMark' @@ -313,6 +311,7 @@ const WorkspaceSkeleton = InvoiceInboxSkeleton export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) { const { toast } = useToast() const t = useTranslations('inbox_workspace') + const tStart = useTranslations('start_cards') const fileInputRef = useRef(null) // Its own input: sharing the header's would upload without the purchase. const purchaseFileInputRef = useRef(null) @@ -1572,16 +1571,20 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) { // So: compact card on mobile only, quiet empty state on desktop. showOnboarding ? ( <> -
- fileInputRef.current?.click()} +
+ fileInputRef.current?.click(), + }} onDismiss={handleDismissOnboarding} - isActivating={isRotating} - compact + dismissLabel={tStart('inbox_dismiss')} />
@@ -1721,16 +1724,27 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) { onRetry={() => { void loadDocument(selected.id, selected.document_id) }} /> ) : showOnboarding ? ( -
- fileInputRef.current?.click()} - onDismiss={handleDismissOnboarding} - isActivating={isRotating} - /> +
+
+ fileInputRef.current?.click(), + }} + onDismiss={handleDismissOnboarding} + dismissLabel={tStart('inbox_dismiss')} + /> +

+ {t('drop_anywhere_hint')} +

+
) : ( void - onUploadClick: () => void - onDismiss: () => void - isActivating: boolean - compact?: boolean -} - -function OnboardingCard({ - hasInboxAddress, - hasAnyItem, - hasResolvedItem, - onActivateInbox, - onUploadClick, - onDismiss, - isActivating, - compact = false, -}: OnboardingCardProps) { - // The card described the page as it was before the underlag rebuild: three - // steps ending at "matcha eller bokför", with no mention that the page now - // searches the mailboxes itself, lists the purchases that are missing a - // receipt, or proposes the kontering. It also carried a Beta badge it had - // outgrown. - const steps = [ - { - done: hasInboxAddress, - title: 'Aktivera din inkorgsadress', - hint: 'En egen adress som leverantörer kan fakturera direkt, och som du kan vidarebefordra kvitton till.', - }, - { - done: hasAnyItem, - title: 'Koppla en brevlåda, maila in, eller ladda upp', - hint: 'Med en kopplad brevlåda letar Kvittojakten själv upp kvitton till köp som saknar underlag. Utan den fyller du på för hand.', - }, - { - done: hasResolvedItem, - title: 'Godkänn konteringen', - hint: 'Matchade underlag får ett förslag på hur de bokförs, utifrån hur du bokfört samma leverantör förut. Du granskar och bokför.', - }, - ] - // First incomplete step drives the active CTA. Falls back to -1 if all done - // (the parent should have hidden the card by then, but guard anyway). - const currentStep = steps.findIndex((s) => !s.done) - - return ( -
- - -
-
-

- Så funkar Underlag -

-
-

- Här samlas underlagen, och här syns köpen som saknar ett. Kvittojakten - söker igenom kopplade brevlådor efter kvitton du inte fått in, och - matchade underlag får ett förslag på kontering att godkänna. Att samla - underlag är alltid gratis; AI-tolkning och kvittojakt ingår i - abonnemanget. -

-
- -
    - {steps.map((step, i) => { - const isDone = step.done - const isCurrent = !isDone && i === currentStep - return ( -
  1. - - {isDone ? ( - - ) : isCurrent ? ( - - {i + 1} - - ) : ( - - )} - -
    -

    - {step.title} -

    - {!isDone && ( -

    - {step.hint} -

    - )} -
    -
  2. - ) - })} -
- - {/* CTA matches the current step. No CTA for step 3: it requires the user to pick a row. */} - {currentStep === 0 && ( - - )} - {currentStep === 1 && ( -
- -

- …eller maila underlagen till din inkorgsadress. -

-
- )} - {currentStep === 2 && ( -

- Välj en post i listan för att matcha eller bokföra. -

- )} -
- ) -} function EmptyPreview({ onUploadClick, diff --git a/components/transactions/InboxZeroState.tsx b/components/transactions/InboxZeroState.tsx index d4e504de..e9d15e7f 100644 --- a/components/transactions/InboxZeroState.tsx +++ b/components/transactions/InboxZeroState.tsx @@ -3,6 +3,7 @@ import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { EmptyState } from '@/components/ui/empty-state' +import { StartCard } from '@/components/dashboard/StartCard' import { Check, Upload, Plus } from 'lucide-react' import Link from 'next/link' @@ -13,22 +14,24 @@ interface InboxZeroStateProps { export default function InboxZeroState({ hasTransactions, onCreateTransaction }: InboxZeroStateProps) { const t = useTranslations('tx_inbox_zero') + const tStart = useTranslations('start_cards') if (!hasTransactions) { - // No transactions at all + // No transactions at all: the start card leads with the bank connection + // (the import page's own "Rekommenderat" path) and keeps the bank-file + // import as the alternative. Manual creation stays reachable from the + // header split button. return ( - - - - +
+ +
) } diff --git a/messages/en.json b/messages/en.json index b87a7e9e..32a93fe8 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3118,6 +3118,7 @@ "purchase_searched_hint": "We searched the mailboxes. Drop the receipt here, or forward it to the inbox address.", "purchase_open_portal": "Open {vendor}", "purchase_drop": "Drop the file here, or click to choose", + "drop_anywhere_hint": "… or drop files anywhere on the page", "purchase_uploading": "Uploading…", "purchase_kind": "Purchase without a document", "purchase_date": "Date", @@ -7102,6 +7103,41 @@ "preset_reports_action": "Create invoice", "preset_reports_secondary": "Import transactions" }, + "start_cards": { + "invoices_title": "Your first invoice takes two minutes.", + "invoices_body": "Send it by email, and the payment is matched to the invoice when it arrives.", + "invoices_primary": "Fetch customers and invoices", + "invoices_secondary": "Create invoice", + "transactions_title": "Connect your bank and the books stay a step ahead.", + "transactions_body": "New transactions arrive every night and are matched to invoices and receipts; you approve with one click.", + "transactions_primary": "Connect bank account", + "transactions_secondary": "Import bank file", + "inbox_title": "Drop the receipt here, and it's done.", + "inbox_body": "Snap a photo, email it or drop the file here: the amount is read and matched to the right transaction.", + "inbox_primary": "Connect mailboxes", + "inbox_secondary": "Upload document", + "inbox_dismiss": "Dismiss", + "salary_eyebrow_start": "Get started", + "salary_eyebrow_next": "Next step", + "salary_title": "The 25th takes care of itself.", + "salary_body_no_employees": "Add your first employee: tax, contributions and AGI are calculated and booked.", + "salary_body_ready": "{count, plural, one {One employee is in place} other {# employees are in place}}: tax, contributions and AGI are calculated and booked.", + "salary_primary_add": "Add employee", + "salary_primary_run": "Start payroll run", + "bookkeeping_title": "Your bookkeeping moves in within five minutes.", + "bookkeeping_body": "Move in from your previous software: fiscal years, the chart of accounts and every voucher come along.", + "bookkeeping_primary": "Fetch from another system", + "bookkeeping_secondary": "Import SIE file", + "bookkeeping_mini_mall_title": "From a template", + "bookkeeping_mini_mall_body": "Rent, salary and other recurring events in one click.", + "bookkeeping_mini_assistent_title": "With the assistant", + "bookkeeping_mini_assistent_body": "Describe the event in words and get the posting suggested.", + "bookkeeping_mini_tomt_title": "Blank voucher", + "bookkeeping_mini_tomt_body": "Line by line, with the next voucher number ready.", + "skattekonto_title": "Your tax account reconciles itself.", + "skattekonto_body": "Connect with BankID and the balance and events are fetched continuously, ready to book.", + "skattekonto_primary": "Connect Skatteverket" + }, "skattekonto": { "help_text": "The balance and events are fetched from Skatteverket and synced automatically every night. Completed events are booked against 1630 Skattekonto, usually automatically; anything that cannot be matched is flagged in the list. Pay in via bankgiro 5050-1055 with your OCR number.", "copy": "copy", diff --git a/messages/sv.json b/messages/sv.json index 7c749fd5..f7f2351e 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -3119,6 +3119,7 @@ "purchase_open_portal": "Öppna {vendor}", "purchase_drop": "Släpp filen här, eller klicka för att välja", "purchase_uploading": "Laddar upp…", + "drop_anywhere_hint": "… eller släpp filer var som helst på sidan", "purchase_kind": "Köp utan underlag", "purchase_date": "Datum", "purchase_amount": "Belopp", @@ -7102,6 +7103,41 @@ "preset_reports_action": "Skapa faktura", "preset_reports_secondary": "Importera transaktioner" }, + "start_cards": { + "invoices_title": "Din första faktura tar två minuter.", + "invoices_body": "Skicka med ett mejl, och betalningen matchas mot fakturan när den kommer in.", + "invoices_primary": "Hämta kunder och fakturor", + "invoices_secondary": "Skapa faktura", + "transactions_title": "Koppla banken, så ligger bokföringen steget före.", + "transactions_body": "Nya transaktioner hämtas varje natt och matchas mot fakturor och kvitton; du godkänner med ett klick.", + "transactions_primary": "Koppla bankkonto", + "transactions_secondary": "Importera bankfil", + "inbox_title": "Släpp kvittot här, sen är det klart.", + "inbox_body": "Fota, mejla eller släpp filen här: beloppet läses av och matchas mot rätt transaktion.", + "inbox_primary": "Koppla brevlådor", + "inbox_secondary": "Ladda upp underlag", + "inbox_dismiss": "Stäng", + "salary_eyebrow_start": "Kom igång", + "salary_eyebrow_next": "Nästa steg", + "salary_title": "Den 25:e sköter sig själv.", + "salary_body_no_employees": "Lägg till din första anställda: skatt, avgifter och AGI räknas fram och bokförs.", + "salary_body_ready": "{count, plural, one {En anställd är på plats} other {# anställda är på plats}}: skatt, avgifter och AGI räknas fram och bokförs.", + "salary_primary_add": "Lägg till anställd", + "salary_primary_run": "Starta lönekörning", + "bookkeeping_title": "Bokföringen flyttar in på fem minuter.", + "bookkeeping_body": "Flytta in från ditt förra program: räkenskapsår, kontoplan och alla verifikat följer med.", + "bookkeeping_primary": "Hämta från annat system", + "bookkeeping_secondary": "Importera SIE-fil", + "bookkeeping_mini_mall_title": "Från mall", + "bookkeeping_mini_mall_body": "Hyra, lön och andra återkommande händelser på ett klick.", + "bookkeeping_mini_assistent_title": "Med assistenten", + "bookkeeping_mini_assistent_body": "Beskriv händelsen med ord och få konteringen föreslagen.", + "bookkeeping_mini_tomt_title": "Tomt verifikat", + "bookkeeping_mini_tomt_body": "Rad för rad, med nästa verifikatnummer redo.", + "skattekonto_title": "Skattekontot stämmer av sig självt.", + "skattekonto_body": "Anslut med BankID så hämtas saldo och händelser löpande, färdiga att bokföra.", + "skattekonto_primary": "Anslut Skatteverket" + }, "skattekonto": { "help_text": "Saldot och händelserna hämtas från Skatteverket och synkas automatiskt varje natt. Genomförda händelser bokförs mot 1630 Skattekonto, oftast automatiskt; det som inte kan matchas flaggas i listan. Betala in via bankgiro 5050-1055 med ditt OCR-nummer.", "copy": "kopiera", diff --git a/public/startkort/abacus.webp b/public/startkort/abacus.webp new file mode 100644 index 00000000..10021ba6 Binary files /dev/null and b/public/startkort/abacus.webp differ diff --git a/public/startkort/geese.webp b/public/startkort/geese.webp new file mode 100644 index 00000000..b60482bf Binary files /dev/null and b/public/startkort/geese.webp differ diff --git a/public/startkort/ledger.webp b/public/startkort/ledger.webp new file mode 100644 index 00000000..869b4a25 Binary files /dev/null and b/public/startkort/ledger.webp differ diff --git a/public/startkort/sthlm.webp b/public/startkort/sthlm.webp new file mode 100644 index 00000000..1cfaa1c0 Binary files /dev/null and b/public/startkort/sthlm.webp differ diff --git a/public/startkort/stopwatch.webp b/public/startkort/stopwatch.webp new file mode 100644 index 00000000..13db78ca Binary files /dev/null and b/public/startkort/stopwatch.webp differ diff --git a/public/startkort/venice.webp b/public/startkort/venice.webp new file mode 100644 index 00000000..61c5e49e Binary files /dev/null and b/public/startkort/venice.webp differ