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
+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} />