feat(ux): company context in settings, Kundfakturor rename, compact verifikat view (#1071)

* feat(ux): company context in settings, Kundfakturor rename, compact verifikat view

Support feedback (2026-07-19): active company invisible in settings,
menu said Fakturor next to Leverantorsfakturor, no compact verifikat view.

- ActiveCompanyBadge chip in the settings modal header and the full-page
  settings header; the modal covers the sidebar CompanySwitcher
- nav + page title Fakturor -> Kundfakturor (sv), Invoices -> Customer
  invoices (en); command palette gets a Kundfakturor page entry
- verifikat list density toggle (comfortable/compact), persisted per
  company like the existing sort/page-size choices

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: decision log for scoped Kundfakturor rename

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): badge hover reveals full company name; pure density state updater

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-20 11:02:04 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent ebf69d2933
commit 90e7c7f47f
8 changed files with 92 additions and 7 deletions
+1
View File
@@ -220,3 +220,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-07-17] Removed 'bokslut' deadline type (replaced by statutory 'arsstamma', ABL 7:10, 6 months): the 3-month milestone had no legal basis and its broken-FY date math was off by one month (May-start FY got 31 Aug; Nov-start rolled "Feb 31" into March). Completed bokslut rows kept for history; type removed from the union like the earlier 'moms'/'inkomstdeklaration' retirements.
[2026-07-17] EU-trade/PS settings stay opt-in flags; a ledger-derived signal (postings on 3108/3308/3107, 15 months) only renders a suggestion callout in tax settings. Auto-flipping registration flags from ledger data would assert a Skatteverket registration we cannot know.
[2026-07-19] Voucher-sequence resync run on prod via execute_sql BEFORE the migration merges: data-only idempotent DML (no schema_migrations orphan risk) and a user was hard-blocked on year-end; migration file 20260719100000 ships the same SQL so every environment replays it as a no-op.
[2026-07-20] Menu rename Fakturor -> Kundfakturor scoped to nav label + /invoices page title + command palette only (support tip 2026-07-19): API-key scope labels, AR-ledger xlsx sheet name, and customer/supplier detail section headings keep "Fakturor" since their surrounding context already disambiguates, and renaming API scope labels would churn a stable admin surface.
+2 -1
View File
@@ -5,6 +5,7 @@ import { useSearchParams, useRouter } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { PageHeader } from '@/components/ui/page-header'
import { SettingsShell } from '@/components/settings/SettingsShell'
import { ActiveCompanyBadge } from '@/components/settings/ActiveCompanyBadge'
const TAB_TO_ROUTE: Record<string, string> = {
company: '/settings/company',
@@ -36,7 +37,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
return (
<div className="space-y-8">
<PageHeader title={t('aria_label')} />
<PageHeader title={t('aria_label')} action={<ActiveCompanyBadge />} />
<SettingsShell variant="page">{children}</SettingsShell>
</div>
)
+40 -1
View File
@@ -32,7 +32,7 @@ import {
STORAGE_KEY_PREFIX as FISCAL_YEAR_STORAGE_KEY_PREFIX,
ALL_YEARS_VALUE as FISCAL_YEAR_ALL_VALUE,
} from '@/components/common/FiscalYearSelector'
import { ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Paperclip, AlertTriangle, CircleSlash, Loader2, BookOpen, X, Copy, Lock, Search, SlidersHorizontal, RotateCcw } from 'lucide-react'
import { ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Paperclip, AlertTriangle, CircleSlash, Loader2, BookOpen, X, Copy, Lock, Search, SlidersHorizontal, RotateCcw, Rows3 } from 'lucide-react'
import { formatDate, formatCurrency } from '@/lib/utils'
import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver'
import { resolveCurrentPeriodId } from '@/lib/bookkeeping/suggest-fiscal-period'
@@ -67,6 +67,10 @@ type SortBy = 'date_desc' | 'date_asc' | 'voucher_asc' | 'voucher_desc'
const SORT_STORAGE_KEY_PREFIX = 'Accounted:journal-sort:'
const SORT_VALUES = new Set<SortBy>(['date_desc', 'date_asc', 'voucher_asc', 'voucher_desc'])
// Compact row density (support feedback: "kompakt visning av verifikat").
// Persisted per company, mirroring the sort key convention.
const DENSITY_STORAGE_KEY_PREFIX = 'Accounted:journal-density:'
// Page-size selector. Persisted per company, mirroring the sort key convention.
// 'all' fetches everything in the current scope (capped server-side at MAX_LIMIT);
// the numeric options paginate normally.
@@ -124,6 +128,7 @@ export default function JournalEntryList() {
const [draftCount, setDraftCount] = useState(0)
const [pageSizeChoice, setPageSizeChoice] = useState<PageSizeChoice>('20')
const [pageSizeHydrated, setPageSizeHydrated] = useState(false)
const [compact, setCompact] = useState(false)
const showingAll = pageSizeChoice === 'all'
const pageSize = showingAll ? ALL_PAGE_SIZE : Number(pageSizeChoice)
@@ -230,6 +235,26 @@ export default function JournalEntryList() {
setSortHydrated(true)
}, [company?.id])
// Restore the persisted row density (per company). Purely visual, so it
// doesn't gate the first fetch; the effect-read avoids an SSR mismatch.
useEffect(() => {
if (typeof window !== 'undefined') {
const stored = window.localStorage.getItem(DENSITY_STORAGE_KEY_PREFIX + (company?.id ?? 'default'))
setCompact(stored === 'compact')
}
}, [company?.id])
const toggleDensity = () => {
const next = !compact
setCompact(next)
if (typeof window !== 'undefined') {
window.localStorage.setItem(
DENSITY_STORAGE_KEY_PREFIX + (company?.id ?? 'default'),
next ? 'compact' : 'comfortable',
)
}
}
// Restore the persisted page-size choice (per company). Same hydration pattern
// as the sort order, read in an effect to avoid an SSR mismatch, and gate the
// first fetch so the list is fetched once at the saved size.
@@ -876,6 +901,19 @@ export default function JournalEntryList() {
</DialogFooter>
</DialogContent>
</Dialog>
{/* Row density: comfortable vs compact, persisted per company. A toggle
(aria-pressed) rather than two modes buried in the filter dialog. */}
<Button
variant="outline"
size="sm"
onClick={toggleDensity}
aria-pressed={compact}
aria-label={t('density_compact')}
title={t('density_compact')}
className={`h-8 w-8 shrink-0 p-0 ${compact ? 'bg-secondary text-foreground' : 'text-muted-foreground'}`}
>
<Rows3 className="h-3.5 w-3.5" />
</Button>
{/* Active fiscal-year scope as a direct one-click picker, pushed to the
right of the control bar, keeps the räkenskapsår visible per BFL and
changeable in one click. Distinct from Filtrera (which now holds only
@@ -1003,6 +1041,7 @@ export default function JournalEntryList() {
key={entry.id}
selected={selectedIds.has(entry.id)}
expanded={isExpanded}
rowClassName={compact ? 'py-1' : undefined}
onClick={() => toggleExpand(entry.id)}
leading={
selectable ? (
+1
View File
@@ -48,6 +48,7 @@ const ACTION_ENTRIES: Entry[] = [
const PAGE_ENTRIES: Entry[] = [
{ id: 'kunder', label: 'Kunder', icon: Users, href: '/customers' },
{ id: 'leverantörer', label: 'Leverantörer', icon: Building2, href: '/suppliers' },
{ id: 'kundfakturor', label: 'Kundfakturor', icon: ReceiptText, href: '/invoices', keywords: 'fakturor fakturering invoices kundfaktura' },
{ id: 'leverantörsfakturor', label: 'Leverantörsfakturor', icon: Wallet, href: '/supplier-invoices' },
{ id: 'bokföring', label: 'Bokföring', icon: BookOpen, href: '/bookkeeping', keywords: 'verifikat journal ledger' },
{ id: 'kontoplan', label: 'Kontoplan', icon: ListTree, href: '/chart-of-accounts', keywords: 'kontoplan konton bas chart of accounts konto' },
@@ -0,0 +1,35 @@
'use client'
import { Building2 } from 'lucide-react'
import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
import { useCompany } from '@/contexts/CompanyContext'
import { Badge } from '@/components/ui/badge'
/**
* Quiet chip naming the active company. Settings surfaces (the routed modal
* and the full-page fallback) cover or dim the sidebar's CompanySwitcher, so
* without this the user edits company-scoped settings with no visible answer
* to "which company am I on?" (support feedback 2026-07-19).
*/
export function ActiveCompanyBadge({ className }: { className?: string }) {
const { company } = useCompany()
const t = useTranslations('common')
if (!company) return null
return (
<Badge
variant="outline"
className={cn(
'max-w-full min-w-0 gap-1.5 font-normal text-muted-foreground',
className,
)}
title={`${t('active_company')}: ${company.name}`}
>
<Building2 className="h-3 w-3 shrink-0" aria-hidden="true" />
<span className="sr-only">{t('active_company')}: </span>
<span className="truncate">{company.name}</span>
</Badge>
)
}
+5 -1
View File
@@ -11,6 +11,7 @@ import {
} from '@/components/ui/dialog'
import { SETTINGS_SECTIONS } from './sections'
import { SettingsShell } from './SettingsShell'
import { ActiveCompanyBadge } from './ActiveCompanyBadge'
/**
* The settings popup. Rendered only by the intercepting route
@@ -52,10 +53,13 @@ export function SettingsModal({ sectionId }: { sectionId?: string }) {
<DialogContent
className="flex h-[100dvh] max-h-[100dvh] max-w-none flex-col gap-0 overflow-hidden rounded-none p-0 md:h-auto md:max-h-[85dvh] md:max-w-4xl md:rounded-lg"
>
<div className="flex shrink-0 items-center border-b border-border px-6 py-4">
<div className="flex shrink-0 items-center gap-3 border-b border-border px-6 py-4">
<DialogTitle className="font-display text-lg tracking-tight">
{t('title')}
</DialogTitle>
{/* The modal covers the sidebar's CompanySwitcher, so the active
company must stay visible here (mr-6 clears the close button). */}
<ActiveCompanyBadge className="ml-auto mr-6" />
</div>
<DialogDescription className="sr-only">{t('description')}</DialogDescription>
<SettingsShell variant="modal" activeSection={resolved} />
+4 -2
View File
@@ -1,5 +1,6 @@
{
"common": {
"active_company": "Active company",
"update_available": "A new version is available.",
"reload": "Reload",
"save": "Save",
@@ -71,7 +72,7 @@
"bureau": "Bureau",
"kpi": "KPIs",
"invoice_inbox": "Documents",
"invoices": "Invoices",
"invoices": "Customer invoices",
"sales_orders": "Orders",
"customers": "Customers",
"articles": "Articles",
@@ -3490,6 +3491,7 @@
"journal_list": {
"mode_vouchers": "Vouchers",
"mode_drafts": "Drafts",
"density_compact": "Compact view",
"show_correction_chain": "Show storno & corrected entries",
"loading": "Loading journal entries...",
"empty_title": "No journal entries",
@@ -4683,7 +4685,7 @@
"validation_min_one_row": "At least one row is required"
},
"invoices": {
"title": "Invoices",
"title": "Customer invoices",
"recurring": "Recurring",
"new_invoice": "New invoice",
"new_self_billed": "Self-billing",
+4 -2
View File
@@ -1,5 +1,6 @@
{
"common": {
"active_company": "Aktivt företag",
"update_available": "En ny version finns tillgänglig.",
"reload": "Ladda om",
"save": "Spara",
@@ -71,7 +72,7 @@
"bureau": "Byrå",
"kpi": "Nyckeltal",
"invoice_inbox": "Underlag",
"invoices": "Fakturor",
"invoices": "Kundfakturor",
"sales_orders": "Order",
"customers": "Kunder",
"articles": "Artiklar",
@@ -3490,6 +3491,7 @@
"journal_list": {
"mode_vouchers": "Verifikat",
"mode_drafts": "Utkast",
"density_compact": "Kompakt visning",
"show_correction_chain": "Visa storno- och rättade poster",
"loading": "Laddar verifikationer...",
"empty_title": "Inga verifikationer",
@@ -4683,7 +4685,7 @@
"validation_min_one_row": "Minst en rad krävs"
},
"invoices": {
"title": "Fakturor",
"title": "Kundfakturor",
"recurring": "Återkommande",
"new_invoice": "Ny faktura",
"new_self_billed": "Självfaktura",