fix(reconciliation): Bankavstämning phase 0 — correctness + feedback batch (+ nav IA regrouping) (#879)
* feat(nav): interaction-mode sidebar grouping — Arbeta/Analys/Data/Skatt & bokslut Nav IA redesign phase 0 (dev_docs/nav_ia_redesign.md): same routes, regrouped by what the user is doing. CLAUDE.md restructured around Hard Rules (doc references updated); pending-page explainer removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): correctness + feedback batch for Bankavstämning (phase 0) Engine: fetchAllRows pagination on status/run/RPC fetches (silent 1000-row cap corrupted totals), optimistic-lock guards on manualLink + apply, unlink audit rows attributed to the acting user (was: company UUID), selected_matches partial apply intersected with a fresh match run. View: silent in-place refresh instead of a full-page skeleton per action, checkbox-gated apply with confidence badges (fuzzy unticked) in chunks of 500, honest result toasts, dry-run errors surfaced, ranked per-row picker candidates pinned to the applied date window, currency-correct amounts (bank side in account currency, GL side SEK), voucher links, translated source types, colored differens, dirty-date-filter guard. Discovery: year-end preflight 404 href fixed (/reconciliation/bank never existed), ⌘K palette entry, real links from the transactions page. v1: status registry schema now matches the actual ReconciliationStatus payload, errors documented as a count, false ~0.85-threshold pitfall replaced, route test mocks the real shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
59ecaee650
commit
ea236cbcdf
@@ -57,6 +57,7 @@ const PAGE_ENTRIES: Entry[] = [
|
||||
{ id: 'rapport-moms', label: 'Visa rapport: Momsdeklaration', icon: BarChart3, href: '/reports/vat-declaration', keywords: 'rapport moms vat deklaration' },
|
||||
{ id: 'rapport-huvudbok', label: 'Visa rapport: Huvudbok', icon: BookOpen, href: '/reports/huvudbok', keywords: 'rapport huvudbok ledger general konto saldo transaktioner per konto kontoutdrag kontoanalys kontokort kontohistorik balance account statement transactions' },
|
||||
{ id: 'rapport-kundreskontra', label: 'Visa rapport: Kundreskontra', icon: Users, href: '/reports/kundreskontra', keywords: 'rapport kundreskontra ar kundfordringar' },
|
||||
{ id: 'rapport-bankavstamning', label: 'Bankavstämning', hint: 'Stäm av bank mot bokföring', icon: ArrowLeftRight, href: '/reports/bank-reconciliation', keywords: 'avstämning stäm av bank matcha banktransaktioner reconcile reconciliation 1930' },
|
||||
{ id: 'importera', label: 'Importera', icon: Upload, href: '/import' },
|
||||
{ id: 'granskning', label: 'Granskning', icon: ClipboardCheck, href: '/pending', keywords: 'pending review' },
|
||||
{ id: 'löner', label: 'Löner', icon: HandCoins, href: '/salary' },
|
||||
|
||||
@@ -33,6 +33,10 @@ import {
|
||||
Tags,
|
||||
ChevronsUpDown,
|
||||
Sparkles,
|
||||
Percent,
|
||||
Landmark,
|
||||
CalendarClock,
|
||||
FileCheck,
|
||||
} from 'lucide-react'
|
||||
import { getBranding } from '@/lib/branding/service'
|
||||
import { ENABLED_EXTENSION_IDS as _ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
|
||||
@@ -105,20 +109,29 @@ type NavLabelKey =
|
||||
| 'import'
|
||||
| 'salary'
|
||||
| 'employees'
|
||||
| 'vat_declaration'
|
||||
| 'skattekonto'
|
||||
| 'deadlines'
|
||||
| 'year_end'
|
||||
| 'help'
|
||||
| 'settings'
|
||||
|
||||
// New nav layout (May 2026):
|
||||
// Nav layout (July 2026 — interaction-mode grouping, dev_docs/nav_ia_redesign.md
|
||||
// Phase 0): same routes as before, regrouped by what the user is doing.
|
||||
// top-of-sidebar — CompanySwitcher (active company / org context).
|
||||
// top section — flat, no dropdown: Hem (agent), Underlag,
|
||||
// Transaktioner, Granskning.
|
||||
// four dropdowns — Försäljning, Inköp, Redovisning, Personal.
|
||||
// top section — flat, no dropdown: Hem, Assistent. Hem doubles as
|
||||
// the "what needs me" surface until a dedicated
|
||||
// /inbox exists.
|
||||
// four dropdowns — Arbeta (produce: the bookkeeping funnel first,
|
||||
// then invoices, supplier invoices, payroll),
|
||||
// Analys (KPI + reports), Data (master-data
|
||||
// registers + import/export), Skatt & bokslut
|
||||
// (statutory: VAT, tax account, deadlines, year-end).
|
||||
// bottom-left popover — signed-in user's name + initial, opens upward
|
||||
// to Inställningar, Hjälp, Support, Logga ut.
|
||||
// Help + Settings are NOT in `navItems` anymore; they live in the account
|
||||
// popover. KPI moved from main to redovisning. Pending stays visible at all
|
||||
// times — the inline badge carries the count.
|
||||
type GroupKey = 'top' | 'försäljning' | 'inköp' | 'redovisning' | 'personal'
|
||||
// Help + Settings are NOT in `navItems`; they live in the account popover.
|
||||
// Pending (Granskning) stays visible at all times — the badge carries the count.
|
||||
type GroupKey = 'top' | 'arbeta' | 'analys' | 'data' | 'skatt'
|
||||
|
||||
interface NavItem {
|
||||
href: string
|
||||
@@ -143,30 +156,42 @@ const navItems: NavItem[] = [
|
||||
// Top section — flat list, always visible, no header
|
||||
{ href: '/', labelKey: 'home', icon: Home, group: 'top' },
|
||||
{ href: '/chat', labelKey: 'assistant', icon: Sparkles, group: 'top' },
|
||||
{ href: '/e/general/invoice-inbox', labelKey: 'invoice_inbox', icon: Inbox, group: 'top' },
|
||||
{ href: '/transactions', labelKey: 'transactions', icon: ArrowLeftRight, group: 'top' },
|
||||
{ href: '/pending', labelKey: 'review', icon: ClipboardCheck, group: 'top' },
|
||||
// Försäljning dropdown
|
||||
{ href: '/invoices', labelKey: 'invoices', icon: ReceiptText, group: 'försäljning' },
|
||||
{ href: '/customers', labelKey: 'customers', icon: Users, group: 'försäljning' },
|
||||
{ href: '/articles', labelKey: 'articles', icon: Tag, group: 'försäljning' },
|
||||
// Inköp dropdown
|
||||
{ href: '/supplier-invoices', labelKey: 'supplier_invoices', icon: Wallet, group: 'inköp' },
|
||||
{ href: '/suppliers', labelKey: 'suppliers', icon: Building2, group: 'inköp' },
|
||||
// Redovisning dropdown
|
||||
{ href: '/kpi', labelKey: 'kpi', icon: TrendingUp, group: 'redovisning' },
|
||||
{ href: '/bookkeeping', labelKey: 'bookkeeping', icon: BookOpen, group: 'redovisning' },
|
||||
{ href: '/chart-of-accounts', labelKey: 'chart_of_accounts', icon: ListTree, group: 'redovisning' },
|
||||
{ href: '/dimensions', labelKey: 'dimensions', icon: Tags, group: 'redovisning', requiresDimensions: true },
|
||||
{ href: '/assets', labelKey: 'assets', icon: Package, group: 'redovisning' },
|
||||
{ href: '/reports', labelKey: 'reports', icon: BarChart3, group: 'redovisning' },
|
||||
{ href: '/import', labelKey: 'import', icon: Upload, group: 'redovisning' },
|
||||
// Personal — "Beta" badge while we validate the end-to-end salary + AGI flow.
|
||||
// Arbeta — everything the user produces. The bookkeeping funnel leads
|
||||
// (Bokföring · Underlag · Transaktioner · Granskning — kept as separate
|
||||
// rows until the unified workspace lands), then the transactional flows.
|
||||
// Löner: "Beta" badge while we validate the end-to-end salary + AGI flow.
|
||||
// employerOnly: shown to aktiebolag and to any employer (pays_salaries), so an
|
||||
// enskild firma that hires staff gets payroll. Owner self-payroll stays
|
||||
// blocked at the engine/DB layer (EF owner takes egna uttag, not lön). #782
|
||||
{ href: '/salary', labelKey: 'salary', icon: HandCoins, group: 'personal', employerOnly: true, betaBadge: true },
|
||||
{ href: '/salary/employees', labelKey: 'employees', icon: Users, group: 'personal', employerOnly: true, betaBadge: true },
|
||||
{ href: '/bookkeeping', labelKey: 'bookkeeping', icon: BookOpen, group: 'arbeta' },
|
||||
{ href: '/e/general/invoice-inbox', labelKey: 'invoice_inbox', icon: Inbox, group: 'arbeta' },
|
||||
{ href: '/transactions', labelKey: 'transactions', icon: ArrowLeftRight, group: 'arbeta' },
|
||||
{ href: '/pending', labelKey: 'review', icon: ClipboardCheck, group: 'arbeta' },
|
||||
{ href: '/invoices', labelKey: 'invoices', icon: ReceiptText, group: 'arbeta' },
|
||||
{ href: '/supplier-invoices', labelKey: 'supplier_invoices', icon: Wallet, group: 'arbeta' },
|
||||
{ href: '/salary', labelKey: 'salary', icon: HandCoins, group: 'arbeta', employerOnly: true, betaBadge: true },
|
||||
// Analys — read the numbers. KPI stays a separate row until the fused
|
||||
// Rapporter surface (nav_ia_redesign §F) is built.
|
||||
{ href: '/kpi', labelKey: 'kpi', icon: TrendingUp, group: 'analys' },
|
||||
{ href: '/reports', labelKey: 'reports', icon: BarChart3, group: 'analys' },
|
||||
// Data — master-data registers + data plumbing. Anställda is a register
|
||||
// (you edit an employee rarely, you run payroll monthly), so it lives here
|
||||
// while the Löner flow stays in Arbeta.
|
||||
{ href: '/customers', labelKey: 'customers', icon: Users, group: 'data' },
|
||||
{ href: '/suppliers', labelKey: 'suppliers', icon: Building2, group: 'data' },
|
||||
{ href: '/articles', labelKey: 'articles', icon: Tag, group: 'data' },
|
||||
{ href: '/salary/employees', labelKey: 'employees', icon: Users, group: 'data', employerOnly: true, betaBadge: true },
|
||||
{ href: '/assets', labelKey: 'assets', icon: Package, group: 'data' },
|
||||
{ href: '/chart-of-accounts', labelKey: 'chart_of_accounts', icon: ListTree, group: 'data' },
|
||||
{ href: '/dimensions', labelKey: 'dimensions', icon: Tags, group: 'data', requiresDimensions: true },
|
||||
{ href: '/import', labelKey: 'import', icon: Upload, group: 'data' },
|
||||
// Skatt & bokslut — everything submitted to the state. Rescues the
|
||||
// previously nav-orphaned /skattekonto and /deadlines, and promotes the
|
||||
// VAT declaration out of the report catalog.
|
||||
{ href: '/reports/vat-declaration', labelKey: 'vat_declaration', icon: Percent, group: 'skatt' },
|
||||
{ href: '/skattekonto', labelKey: 'skattekonto', icon: Landmark, group: 'skatt' },
|
||||
{ href: '/deadlines', labelKey: 'deadlines', icon: CalendarClock, group: 'skatt' },
|
||||
{ href: '/bookkeeping/year-end', labelKey: 'year_end', icon: FileCheck, group: 'skatt' },
|
||||
]
|
||||
|
||||
// Map known extension hrefs to nav translation keys so sidebar labels translate.
|
||||
@@ -178,10 +203,10 @@ function extensionLabelKey(href: string): string | null {
|
||||
}
|
||||
|
||||
const groupLabelKey: Record<Exclude<GroupKey, 'top'>, string> = {
|
||||
försäljning: 'group_sales',
|
||||
inköp: 'group_purchases',
|
||||
redovisning: 'group_accounting',
|
||||
personal: 'group_personnel',
|
||||
arbeta: 'group_work',
|
||||
analys: 'group_analysis',
|
||||
data: 'group_data',
|
||||
skatt: 'group_tax',
|
||||
}
|
||||
|
||||
// Best single-character initial we can show in the bottom-left account
|
||||
@@ -227,10 +252,10 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
// Personal hidden).
|
||||
type ExpandableGroup = Exclude<GroupKey, 'top'>
|
||||
const [manualCollapsed, setManualCollapsed] = useState<Record<ExpandableGroup, boolean>>({
|
||||
försäljning: false,
|
||||
inköp: false,
|
||||
redovisning: false,
|
||||
personal: false,
|
||||
arbeta: false,
|
||||
analys: false,
|
||||
data: false,
|
||||
skatt: false,
|
||||
})
|
||||
const toggleGroup = (g: ExpandableGroup) =>
|
||||
setManualCollapsed((prev) => ({ ...prev, [g]: !prev[g] }))
|
||||
@@ -257,6 +282,15 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
if (href === '/salary') {
|
||||
return pathname === '/salary' || pathname.startsWith('/salary/runs')
|
||||
}
|
||||
// Bokslut (/bookkeeping/year-end) and Moms (/reports/vat-declaration)
|
||||
// have their own rows under Skatt & bokslut — carve them out of their
|
||||
// parent routes so exactly one row lights up.
|
||||
if (href === '/bookkeeping') {
|
||||
return pathname.startsWith('/bookkeeping') && !pathname.startsWith('/bookkeeping/year-end')
|
||||
}
|
||||
if (href === '/reports') {
|
||||
return pathname.startsWith('/reports') && !pathname.startsWith('/reports/vat-declaration')
|
||||
}
|
||||
return pathname.startsWith(href)
|
||||
}
|
||||
|
||||
@@ -385,10 +419,10 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
)
|
||||
|
||||
const sidebarGroups: { key: ExpandableGroup; items: NavItem[] }[] = [
|
||||
{ key: 'försäljning', items: filteredItems.filter((i) => i.group === 'försäljning') },
|
||||
{ key: 'inköp', items: filteredItems.filter((i) => i.group === 'inköp') },
|
||||
{ key: 'redovisning', items: filteredItems.filter((i) => i.group === 'redovisning') },
|
||||
{ key: 'personal', items: filteredItems.filter((i) => i.group === 'personal') },
|
||||
{ key: 'arbeta', items: filteredItems.filter((i) => i.group === 'arbeta') },
|
||||
{ key: 'analys', items: filteredItems.filter((i) => i.group === 'analys') },
|
||||
{ key: 'data', items: filteredItems.filter((i) => i.group === 'data') },
|
||||
{ key: 'skatt', items: filteredItems.filter((i) => i.group === 'skatt') },
|
||||
]
|
||||
|
||||
// A group is expanded when the user hasn't manually collapsed it OR
|
||||
@@ -430,17 +464,12 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
<CompanySwitcher />
|
||||
</div>
|
||||
<nav className="px-3" aria-label={tNav('main_navigation')}>
|
||||
{/* Top section: flat, no header. Hem, Underlag, Transaktioner, Granskning. */}
|
||||
{/* Top section: flat, no header. Hem, Assistent. */}
|
||||
<div className="mb-4 space-y-px">
|
||||
{topItems.map((item) => {
|
||||
const active = isActive(item.href)
|
||||
const enabled = isItemEnabled(item.href)
|
||||
const badge =
|
||||
item.href === '/transactions' && liveUncategorizedTransactionCount > 0
|
||||
? liveUncategorizedTransactionCount
|
||||
: item.href === '/pending' && pendingOperationsCount > 0
|
||||
? pendingOperationsCount
|
||||
: null
|
||||
const badge: number | null = null
|
||||
const decorBadge = renderBadge(item, 'sidebar')
|
||||
const content = (
|
||||
<>
|
||||
@@ -487,7 +516,7 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Collapsible groups: Försäljning, Inköp, Redovisning, Personal */}
|
||||
{/* Collapsible groups: Arbeta, Analys, Data, Skatt & bokslut */}
|
||||
{sidebarGroups
|
||||
.filter(({ items }) => items.length > 0)
|
||||
.map(({ key, items }) => {
|
||||
@@ -512,6 +541,12 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
const Icon = item.icon
|
||||
const active = isActive(item.href)
|
||||
const enabled = isItemEnabled(item.href) && !item.comingSoon
|
||||
const badge =
|
||||
item.href === '/transactions' && liveUncategorizedTransactionCount > 0
|
||||
? liveUncategorizedTransactionCount
|
||||
: item.href === '/pending' && pendingOperationsCount > 0
|
||||
? pendingOperationsCount
|
||||
: null
|
||||
const decorBadge = renderBadge(item, 'sidebar')
|
||||
const content = (
|
||||
<>
|
||||
@@ -524,7 +559,11 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
)}
|
||||
/>
|
||||
<span className="flex-1">{tNav(item.labelKey)}</span>
|
||||
{decorBadge}
|
||||
{decorBadge ? decorBadge : badge !== null && (
|
||||
<span className="ml-auto min-w-[18px] h-[18px] flex items-center justify-center rounded-full bg-primary/15 text-primary text-[10px] font-semibold px-1">
|
||||
{badge > 99 ? '99+' : badge}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
const baseClass = cn(
|
||||
@@ -557,11 +596,11 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{/* Extension nav items land in Redovisning since the
|
||||
current extensions (TIC workspace, etc.) are
|
||||
accounting-adjacent. Future categorised extensions
|
||||
can opt into a different group via their manifest. */}
|
||||
{key === 'redovisning' &&
|
||||
{/* Extension nav items land in Arbeta since extension
|
||||
workspaces are work surfaces. Future categorised
|
||||
extensions can opt into a different group via
|
||||
their manifest. */}
|
||||
{key === 'arbeta' &&
|
||||
visibleExtensionNavItems.map((item) => {
|
||||
const Icon = resolveIcon(item.icon)
|
||||
const active = isActive(item.href)
|
||||
@@ -795,17 +834,12 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="px-2">
|
||||
{/* Top items (Hem, Underlag, Transaktioner, Granskning) */}
|
||||
{/* Top items (Hem, Assistent) */}
|
||||
<div className="space-y-0.5">
|
||||
{topItems.map((item) => {
|
||||
const active = isActive(item.href)
|
||||
const enabled = isItemEnabled(item.href)
|
||||
const badge =
|
||||
item.href === '/transactions' && liveUncategorizedTransactionCount > 0
|
||||
? liveUncategorizedTransactionCount
|
||||
: item.href === '/pending' && pendingOperationsCount > 0
|
||||
? pendingOperationsCount
|
||||
: null
|
||||
const badge: number | null = null
|
||||
const decorBadge = renderBadge(item, 'mobile')
|
||||
const content = (
|
||||
<>
|
||||
@@ -846,7 +880,7 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* AR / AP / Personal / Accounting groups (mobile) */}
|
||||
{/* Arbeta / Analys / Data / Skatt & bokslut groups (mobile) */}
|
||||
{sidebarGroups.filter(({ items }) => items.length > 0).map(({ key, items }) => (
|
||||
<div key={key}>
|
||||
<div className="flex items-center gap-3 my-1.5 px-3">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user