feat: AR/AP navigation separation and bookkeeping improvements (#109)
* feat: separate AR/AP/accounting into distinct nav groups (#92) Split the flat "Finans" sidebar group into three visually distinct sections — Försäljning (AR), Inköp (AP), and Redovisning — so users coming from Fortnox immediately find customer invoicing and supplier invoices as top-level concepts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: journal entry detail view, correction chain, and account name display - Add journal entry detail page at /bookkeeping/[id] with full entry view - Add correction chain API and component showing storno relationships - Add JournalEntryStatusBadge component for entry status display - Show debit/credit account names in template picker and review dialogs - Expand client-side BAS account name mapping with additional accounts - Show account codes on transaction inbox suggestion buttons Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback — N+1 query, duplicate name, nav dedup - Batch reverse-lookup into single query per BFS iteration (was N+1) - Differentiate account 2393 from 2893 in display names - Extract shared loop for desktop/mobile nav group rendering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7ab789d95c
commit
4d4a9a40c9
@@ -57,23 +57,28 @@ const navItems: NavItem[] = [
|
||||
{ href: '/', label: 'Översikt', icon: LayoutDashboard, group: 'main' },
|
||||
{ href: '/kpi', label: 'Nyckeltal', icon: TrendingUp, group: 'main' },
|
||||
{ href: '/deadlines', label: 'Deadlines', icon: Calendar, group: 'main' },
|
||||
{ href: '/invoices', label: 'Fakturor', icon: Receipt, group: 'finans' },
|
||||
{ href: '/customers', label: 'Kunder', icon: Users, group: 'finans' },
|
||||
{ href: '/expenses', label: 'Utgifter', icon: Wallet, group: 'finans' },
|
||||
// AR — Accounts Receivable
|
||||
{ href: '/invoices', label: 'Fakturor', icon: Receipt, group: 'försäljning' },
|
||||
{ href: '/customers', label: 'Kunder', icon: Users, group: 'försäljning' },
|
||||
// AP — Accounts Payable
|
||||
{ href: '/expenses', label: 'Utgifter', icon: Wallet, group: 'inköp' },
|
||||
// Temporarily hidden pending module rework (see feedback #49)
|
||||
{ href: '/suppliers', label: 'Leverantörer', icon: Building2, group: 'finans', hidden: true },
|
||||
{ href: '/supplier-invoices', label: 'Leverantörsfakturor', icon: FileInput, group: 'finans', hidden: true },
|
||||
{ href: '/transactions', label: 'Transaktioner', icon: ArrowLeftRight, group: 'finans' },
|
||||
{ href: '/bookkeeping', label: 'Bokföring', icon: BookOpen, group: 'finans' },
|
||||
{ href: '/reports', label: 'Rapporter', icon: BarChart3, group: 'finans' },
|
||||
{ href: '/import', label: 'Importera', icon: Upload, group: 'finans' },
|
||||
{ href: '/suppliers', label: 'Leverantörer', icon: Building2, group: 'inköp', hidden: true },
|
||||
{ href: '/supplier-invoices', label: 'Leverantörsfakturor', icon: FileInput, group: 'inköp', hidden: true },
|
||||
// General accounting
|
||||
{ href: '/transactions', label: 'Transaktioner', icon: ArrowLeftRight, group: 'redovisning' },
|
||||
{ href: '/bookkeeping', label: 'Bokföring', icon: BookOpen, group: 'redovisning' },
|
||||
{ href: '/reports', label: 'Rapporter', icon: BarChart3, group: 'redovisning' },
|
||||
{ href: '/import', label: 'Importera', icon: Upload, group: 'redovisning' },
|
||||
{ href: '/help', label: 'Hjälp', icon: HelpCircle, group: 'övrigt' },
|
||||
{ href: '/settings', label: 'Inställningar', icon: Settings, group: 'övrigt' },
|
||||
]
|
||||
|
||||
const groupLabels: Record<string, string> = {
|
||||
main: 'Huvudmeny',
|
||||
finans: 'Finans',
|
||||
försäljning: 'Försäljning',
|
||||
inköp: 'Inköp',
|
||||
redovisning: 'Redovisning',
|
||||
övrigt: 'Övrigt',
|
||||
}
|
||||
|
||||
@@ -124,9 +129,15 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
)
|
||||
|
||||
const mainItems = filteredItems.filter(i => i.group === 'main')
|
||||
const finansItems = filteredItems.filter(i => i.group === 'finans')
|
||||
const övrigtItems = filteredItems.filter(i => i.group === 'övrigt')
|
||||
|
||||
// Groups rendered as distinct sidebar sections (AR, AP, Accounting)
|
||||
const sidebarGroups = [
|
||||
{ key: 'försäljning', items: filteredItems.filter(i => i.group === 'försäljning'), spacing: 'mb-4' },
|
||||
{ key: 'inköp', items: filteredItems.filter(i => i.group === 'inköp'), spacing: 'mb-4' },
|
||||
{ key: 'redovisning', items: filteredItems.filter(i => i.group === 'redovisning'), spacing: 'mb-6' },
|
||||
] as const
|
||||
|
||||
const mobileNavItems = [
|
||||
{ href: '/', label: 'Översikt', icon: LayoutDashboard },
|
||||
{ href: '/invoices', label: 'Fakturor', icon: Receipt },
|
||||
@@ -179,44 +190,46 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Finans group */}
|
||||
<div className="mb-6">
|
||||
<p className="px-3 mb-1.5 text-[10px] font-semibold text-muted-foreground uppercase tracking-[0.08em]">
|
||||
{groupLabels.finans}
|
||||
</p>
|
||||
<div className="space-y-px">
|
||||
{finansItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item.href)
|
||||
const badge = item.href === '/transactions' && uncategorizedTransactionCount > 0
|
||||
? uncategorizedTransactionCount
|
||||
: null
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'group flex items-center px-3 py-[7px] text-[13px] transition-colors duration-150 rounded-lg',
|
||||
active
|
||||
? 'bg-primary/12 text-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
|
||||
)}
|
||||
>
|
||||
<Icon className={cn(
|
||||
"mr-2.5 h-[15px] w-[15px] flex-shrink-0",
|
||||
active ? "text-primary" : "text-muted-foreground group-hover:text-foreground"
|
||||
)} />
|
||||
<span className="flex-1">{item.label}</span>
|
||||
{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>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
{/* AR / AP / Accounting groups */}
|
||||
{sidebarGroups.map(({ key, items, spacing }) => (
|
||||
<div key={key} className={spacing}>
|
||||
<p className="px-3 mb-1.5 text-[10px] font-semibold text-muted-foreground uppercase tracking-[0.08em]">
|
||||
{groupLabels[key]}
|
||||
</p>
|
||||
<div className="space-y-px">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item.href)
|
||||
const badge = item.href === '/transactions' && uncategorizedTransactionCount > 0
|
||||
? uncategorizedTransactionCount
|
||||
: null
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'group flex items-center px-3 py-[7px] text-[13px] transition-colors duration-150 rounded-lg',
|
||||
active
|
||||
? 'bg-primary/12 text-foreground font-medium'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
|
||||
)}
|
||||
>
|
||||
<Icon className={cn(
|
||||
"mr-2.5 h-[15px] w-[15px] flex-shrink-0",
|
||||
active ? "text-primary" : "text-muted-foreground group-hover:text-foreground"
|
||||
)} />
|
||||
<span className="flex-1">{item.label}</span>
|
||||
{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>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Övrigt group - collapsible */}
|
||||
<div className="mb-4">
|
||||
@@ -416,43 +429,45 @@ export default function DashboardNav({ companyName, entityType, uncategorizedTra
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Finans divider */}
|
||||
<div className="flex items-center gap-3 my-1.5 px-3">
|
||||
<span className="text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-[0.08em]">Finans</span>
|
||||
<div className="flex-1 h-px bg-border/30" />
|
||||
</div>
|
||||
|
||||
{/* Finance items */}
|
||||
<div className="space-y-0.5">
|
||||
{finansItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item.href)
|
||||
const badge = item.href === '/transactions' && uncategorizedTransactionCount > 0
|
||||
? uncategorizedTransactionCount
|
||||
: null
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={closeMobileMenu}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 min-h-[44px] rounded-lg transition-colors',
|
||||
active
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'text-foreground active:bg-muted/60'
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("h-[18px] w-[18px] flex-shrink-0", active ? "text-primary" : "text-muted-foreground")} />
|
||||
<span className="text-sm flex-1">{item.label}</span>
|
||||
{badge !== null && (
|
||||
<span className="min-w-[20px] h-[20px] flex items-center justify-center rounded-full bg-primary/15 text-primary text-[10px] font-semibold px-1.5">
|
||||
{badge > 99 ? '99+' : badge}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{/* AR / AP / Accounting groups (mobile) */}
|
||||
{sidebarGroups.map(({ key, items }) => (
|
||||
<div key={key}>
|
||||
<div className="flex items-center gap-3 my-1.5 px-3">
|
||||
<span className="text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-[0.08em]">{groupLabels[key]}</span>
|
||||
<div className="flex-1 h-px bg-border/30" />
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{items.map((item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item.href)
|
||||
const badge = item.href === '/transactions' && uncategorizedTransactionCount > 0
|
||||
? uncategorizedTransactionCount
|
||||
: null
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={closeMobileMenu}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-3 min-h-[44px] rounded-lg transition-colors',
|
||||
active
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'text-foreground active:bg-muted/60'
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("h-[18px] w-[18px] flex-shrink-0", active ? "text-primary" : "text-muted-foreground")} />
|
||||
<span className="text-sm flex-1">{item.label}</span>
|
||||
{badge !== null && (
|
||||
<span className="min-w-[20px] h-[20px] flex items-center justify-center rounded-full bg-primary/15 text-primary text-[10px] font-semibold px-1.5">
|
||||
{badge > 99 ? '99+' : badge}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Övrigt divider */}
|
||||
<div className="flex items-center gap-3 my-1.5 px-3">
|
||||
|
||||
Reference in New Issue
Block a user