From 90e7c7f47fc15c572213a3170ff82376a1b20087 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:02:04 +0200 Subject: [PATCH] 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 * docs: decision log for scoped Kundfakturor rename Co-Authored-By: Claude Fable 5 * fix(review): badge hover reveals full company name; pure density state updater Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- DECISIONS.md | 1 + app/(dashboard)/settings/layout.tsx | 3 +- components/bookkeeping/JournalEntryList.tsx | 41 ++++++++++++++++++++- components/common/CommandPalette.tsx | 1 + components/settings/ActiveCompanyBadge.tsx | 35 ++++++++++++++++++ components/settings/SettingsModal.tsx | 6 ++- messages/en.json | 6 ++- messages/sv.json | 6 ++- 8 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 components/settings/ActiveCompanyBadge.tsx diff --git a/DECISIONS.md b/DECISIONS.md index 1332a608..fcfc188d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -220,3 +220,4 @@ One line per decision: `[YYYY-MM-DD] : `. 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. diff --git a/app/(dashboard)/settings/layout.tsx b/app/(dashboard)/settings/layout.tsx index 91fc14b3..e5d60109 100644 --- a/app/(dashboard)/settings/layout.tsx +++ b/app/(dashboard)/settings/layout.tsx @@ -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 = { company: '/settings/company', @@ -36,7 +37,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode return (
- + } /> {children}
) diff --git a/components/bookkeeping/JournalEntryList.tsx b/components/bookkeeping/JournalEntryList.tsx index 1a31035d..053f405a 100644 --- a/components/bookkeeping/JournalEntryList.tsx +++ b/components/bookkeeping/JournalEntryList.tsx @@ -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(['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('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() { + {/* Row density: comfortable vs compact, persisted per company. A toggle + (aria-pressed) rather than two modes buried in the filter dialog. */} + {/* 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 ? ( diff --git a/components/common/CommandPalette.tsx b/components/common/CommandPalette.tsx index e5c707d0..45e8b516 100644 --- a/components/common/CommandPalette.tsx +++ b/components/common/CommandPalette.tsx @@ -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' }, diff --git a/components/settings/ActiveCompanyBadge.tsx b/components/settings/ActiveCompanyBadge.tsx new file mode 100644 index 00000000..b9322565 --- /dev/null +++ b/components/settings/ActiveCompanyBadge.tsx @@ -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 ( + + + ) +} diff --git a/components/settings/SettingsModal.tsx b/components/settings/SettingsModal.tsx index eed73d3d..a3d7df03 100644 --- a/components/settings/SettingsModal.tsx +++ b/components/settings/SettingsModal.tsx @@ -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 }) { -
+
{t('title')} + {/* The modal covers the sidebar's CompanySwitcher, so the active + company must stay visible here (mr-6 clears the close button). */} +
{t('description')} diff --git a/messages/en.json b/messages/en.json index e15ab32d..23b24cc2 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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", diff --git a/messages/sv.json b/messages/sv.json index 7890aace..4fa479ab 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -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",