* fix(bokslut): map computer/vehicle depreciation to standard 7832 so it resolves (#755) The computer asset category mapped depreciation expense to 7833 and vehicle to 7834, but neither is in the standard BAS catalog (7834/7835 were removed as non-standard in #463, guarded by bas-reference.test.ts). Because backfillStandardBASAccounts only seeds accounts present in BAS_REFERENCE, the engine threw AccountsNotInChartError on minimal charts and annual depreciation was blocked. - Remap computer and vehicle depreciation expense to 7832 (Avskrivningar på inventarier, verktyg och installationer). Both 1240 (Bilar) and 1250 (Datorer) sit in the maskiner-och-inventarier asset range, so 7832 is the correct standard depreciation account — same one equipment already uses. The asset register still separates them via 1240/1249 and 1250/1259 on the balance sheet. - A regression guard surfaced a second gap: other_tangible mapped to 1280/1289, but 1280 is 'Pågående nyanläggningar/förskott' and 1289 is not a BAS account. Remap other_tangible to 1290/1299 ('Övriga materiella anläggningstillgångar' + its ack. avskrivningar) — the BAS-correct accounts, and the range the iXBRL K2 mapper already classifies other_tangible under. Keeps accumulated = asset + 9. - Add a guard test asserting every DEFAULT_ACCOUNTS_BY_CATEGORY account resolves in BAS_REFERENCE, so a future missing account fails CI instead of a user's depreciation run. No new BAS accounts are added, so the non-standard-accounts guard stays green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(dashboard): drop the duplicate next-best-action hero for a single CTA The agent-built dashboard showed a 'next best action' hero card AND the unified 'Att göra' worklist below it — two surfaces pointing at the same work (book transactions, unpaid invoices). Remove the hero so the page leads with metrics + the single 'Att göra' worklist, giving one unambiguous CTA surface instead of two. Drops the now-unused nextBestAction computation and the Receipt/ ArrowLeftRight/Clock imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
663e1232a4
commit
cb01b946fc
@@ -11,11 +11,8 @@ import { TaxTodoWidget } from '@/components/deadlines/TaxTodoWidget'
|
||||
import NewUserChecklist from '@/components/onboarding/NewUserChecklist'
|
||||
import AttGoraSection from '@/components/dashboard/AttGoraSection'
|
||||
import {
|
||||
Receipt,
|
||||
ArrowLeftRight,
|
||||
ChevronRight,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
ArrowRight,
|
||||
MessageCircle,
|
||||
} from 'lucide-react'
|
||||
@@ -114,67 +111,15 @@ export default function DashboardContent({ companyId, summary, worklist, suggest
|
||||
// header so the tile and the section never disagree.
|
||||
const todoCount = worklist.total + (summary.expiringBankConnections?.length ?? 0)
|
||||
|
||||
// Pick the single most-urgent next action so the launchpad surfaces one
|
||||
// unambiguous CTA. Order matches the friction we actually want to remove
|
||||
// first: stale → overdue → uncategorized → unpaid → all clear.
|
||||
const nextBestAction = (() => {
|
||||
if (summary.staleUncategorizedCount > 0) {
|
||||
return {
|
||||
href: '/transactions',
|
||||
title: 'Gamla transaktioner väntar',
|
||||
body: `${summary.staleUncategorizedCount} transaktion${summary.staleUncategorizedCount === 1 ? '' : 'er'} äldre än 14 dagar saknar bokföring.`,
|
||||
cta: 'Bokför nu',
|
||||
tone: 'destructive' as const,
|
||||
icon: Clock,
|
||||
}
|
||||
}
|
||||
if (summary.overdueInvoicesCount > 0) {
|
||||
return {
|
||||
href: '/invoices?status=unpaid',
|
||||
title: 'Förfallna fakturor',
|
||||
body: `${summary.overdueInvoicesCount} st · ${formatCurrency(summary.unpaidInvoicesTotal)}`,
|
||||
cta: 'Gå till fakturor',
|
||||
tone: 'destructive' as const,
|
||||
icon: Receipt,
|
||||
}
|
||||
}
|
||||
if (worklist.counts.book_transaction > 0) {
|
||||
const n = worklist.counts.book_transaction
|
||||
return {
|
||||
href: '/transactions',
|
||||
title: 'Transaktioner att bokföra',
|
||||
body: `${n} obokförd${n === 1 ? '' : 'a'} transaktion${n === 1 ? '' : 'er'}.`,
|
||||
cta: 'Bokför nu',
|
||||
tone: 'primary' as const,
|
||||
icon: ArrowLeftRight,
|
||||
}
|
||||
}
|
||||
if (summary.unpaidInvoicesCount > 0) {
|
||||
return {
|
||||
href: '/invoices?status=unpaid',
|
||||
title: 'Obetalda fakturor',
|
||||
body: `${summary.unpaidInvoicesCount} st · ${formatCurrency(summary.unpaidInvoicesTotal)}`,
|
||||
cta: 'Visa fakturor',
|
||||
tone: 'primary' as const,
|
||||
icon: Receipt,
|
||||
}
|
||||
}
|
||||
return {
|
||||
href: '/invoices/new',
|
||||
title: 'Allt är ikapp',
|
||||
body: 'Inga obokförda transaktioner och inga obetalda fakturor. Skicka nästa faktura?',
|
||||
cta: 'Skapa faktura',
|
||||
tone: 'neutral' as const,
|
||||
icon: CheckCircle2,
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className="stagger-enter space-y-8">
|
||||
{!agentBuilt ? (
|
||||
/* Build-assistant hero — shown until the company has a verified
|
||||
agent_profile. Takes the hero slot so existing/migrated users get a
|
||||
clear prompt instead of a full-screen onboarding takeover. */
|
||||
{/* Build-assistant hero — shown only until the company has a verified
|
||||
agent_profile, so existing/migrated users get a clear prompt instead
|
||||
of a full-screen onboarding takeover. Once the assistant is built the
|
||||
dashboard leads with the metrics + the unified "Att göra" worklist
|
||||
below; we deliberately drop a next-best-action hero here so the page
|
||||
has a single CTA surface instead of two that point at the same work. */}
|
||||
{!agentBuilt && (
|
||||
<section>
|
||||
<Link href="/onboarding/agent" className="block group">
|
||||
<Card className="transition-colors hover:border-primary/50">
|
||||
@@ -199,38 +144,6 @@ export default function DashboardContent({ companyId, summary, worklist, suggest
|
||||
</Card>
|
||||
</Link>
|
||||
</section>
|
||||
) : (
|
||||
/* Next best action — single hero card, shown for every agent-built
|
||||
company regardless of nav density so there is always one clear CTA */
|
||||
<section>
|
||||
<Link href={nextBestAction.href} className="block group">
|
||||
<Card className={cn(
|
||||
'transition-colors',
|
||||
nextBestAction.tone === 'destructive' && 'border-destructive/30 hover:bg-destructive/[0.03]',
|
||||
nextBestAction.tone === 'primary' && 'hover:border-primary/50',
|
||||
nextBestAction.tone === 'neutral' && 'hover:border-primary/30',
|
||||
)}>
|
||||
<CardContent className="p-6 flex items-center gap-5">
|
||||
<div className={cn(
|
||||
'flex-shrink-0 h-10 w-10 rounded-lg flex items-center justify-center',
|
||||
nextBestAction.tone === 'destructive' && 'bg-destructive/10 text-destructive',
|
||||
nextBestAction.tone === 'primary' && 'bg-secondary text-foreground',
|
||||
nextBestAction.tone === 'neutral' && 'bg-secondary text-foreground',
|
||||
)}>
|
||||
<nextBestAction.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-display text-xl leading-tight">{nextBestAction.title}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">{nextBestAction.body}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground group-hover:translate-x-0.5 transition-transform">
|
||||
<span>{nextBestAction.cta}</span>
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Key metrics — 4 compact cards */}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
disposeAsset,
|
||||
updateAsset,
|
||||
} from '../assets/asset-service'
|
||||
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
|
||||
import type { Asset } from '@/types'
|
||||
|
||||
vi.mock('@/lib/bookkeeping/engine', () => ({
|
||||
@@ -23,9 +24,9 @@ describe('DEFAULT_ACCOUNTS_BY_CATEGORY', () => {
|
||||
land_improvement: { asset: '1150', accumulated: '1159', expense: '7824' },
|
||||
machinery: { asset: '1210', accumulated: '1219', expense: '7831' },
|
||||
equipment: { asset: '1220', accumulated: '1229', expense: '7832' },
|
||||
vehicle: { asset: '1240', accumulated: '1249', expense: '7834' },
|
||||
computer: { asset: '1250', accumulated: '1259', expense: '7833' },
|
||||
other_tangible: { asset: '1280', accumulated: '1289', expense: '7839' },
|
||||
vehicle: { asset: '1240', accumulated: '1249', expense: '7832' },
|
||||
computer: { asset: '1250', accumulated: '1259', expense: '7832' },
|
||||
other_tangible: { asset: '1290', accumulated: '1299', expense: '7839' },
|
||||
} as const
|
||||
expect(DEFAULT_ACCOUNTS_BY_CATEGORY).toEqual(expected)
|
||||
})
|
||||
@@ -48,6 +49,22 @@ describe('DEFAULT_ACCOUNTS_BY_CATEGORY', () => {
|
||||
expect(expense).toMatch(/^78\d{2}$/)
|
||||
}
|
||||
})
|
||||
|
||||
// Regression guard for #755: 7833/7834 were referenced here but absent from
|
||||
// the BAS reference, so backfillStandardBASAccounts could not seed them and
|
||||
// annual depreciation threw AccountsNotInChartError. Every account in the
|
||||
// triple must resolve in BAS_REFERENCE — otherwise the lazy backfill silently
|
||||
// can't add it and the depreciation posting fails on minimal charts.
|
||||
it('every account in the triple exists in the BAS reference (backfillable)', () => {
|
||||
for (const cat of Object.keys(DEFAULT_ACCOUNTS_BY_CATEGORY) as Array<
|
||||
keyof typeof DEFAULT_ACCOUNTS_BY_CATEGORY
|
||||
>) {
|
||||
const { asset, accumulated, expense } = DEFAULT_ACCOUNTS_BY_CATEGORY[cat]
|
||||
for (const account of [asset, accumulated, expense]) {
|
||||
expect(getBASReference(account), `${cat}: ${account} missing from BAS reference`).toBeDefined()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('disposeAsset — gain/loss account selection', () => {
|
||||
|
||||
@@ -12,8 +12,16 @@ import type {
|
||||
|
||||
/**
|
||||
* Default BAS account triples per category. The user can override at create
|
||||
* time; these only kick in when the form doesn't specify accounts. Matches
|
||||
* the seeded BAS 2020 chart (lib/bookkeeping/bas-data/).
|
||||
* time; these only kick in when the form doesn't specify accounts. Every
|
||||
* account here MUST exist in BAS_REFERENCE (lib/bookkeeping/bas-data/) so the
|
||||
* engine's backfillStandardBASAccounts can seed it on a minimal chart —
|
||||
* otherwise depreciation throws AccountsNotInChartError (#755). A guard test in
|
||||
* asset-service.test.ts enforces that invariant.
|
||||
*
|
||||
* vehicle (1240) and computer (1250) both sit in the maskiner-och-inventarier
|
||||
* asset range, so their depreciation maps to 7832 (Avskrivningar på
|
||||
* inventarier, verktyg och installationer) — 7833/7834 are not in the standard
|
||||
* BAS catalog (removed as non-standard in #463).
|
||||
*/
|
||||
export const DEFAULT_ACCOUNTS_BY_CATEGORY: Record<
|
||||
AssetCategory,
|
||||
@@ -24,9 +32,9 @@ export const DEFAULT_ACCOUNTS_BY_CATEGORY: Record<
|
||||
land_improvement: { asset: '1150', accumulated: '1159', expense: '7824' },
|
||||
machinery: { asset: '1210', accumulated: '1219', expense: '7831' },
|
||||
equipment: { asset: '1220', accumulated: '1229', expense: '7832' },
|
||||
vehicle: { asset: '1240', accumulated: '1249', expense: '7834' },
|
||||
computer: { asset: '1250', accumulated: '1259', expense: '7833' },
|
||||
other_tangible: { asset: '1280', accumulated: '1289', expense: '7839' },
|
||||
vehicle: { asset: '1240', accumulated: '1249', expense: '7832' },
|
||||
computer: { asset: '1250', accumulated: '1259', expense: '7832' },
|
||||
other_tangible: { asset: '1290', accumulated: '1299', expense: '7839' },
|
||||
}
|
||||
|
||||
export interface CreateAssetInput {
|
||||
|
||||
Reference in New Issue
Block a user