01dbef4015
* feat(dimensions): PR4 reports — dimension-filtered P&L everywhere + Resultat per projekt/kostnadsställe The Project P&L milestone of the dimensions plan (dev_docs §7 PR4). One choke point lights up everything: generateTrialBalance gains options.dimensions (SIE dim → code map) pushed down as jsonb containment (dimensions @>, served by idx_jel_dimensions_gin) on both line queries, with company-wide opening balances dropped when filtered (they cannot be dimension-scoped; P&L-safe by whitelist). Resultatrapport, resultaträkning, huvudbok, monthly-breakdown and the TB drill-down inherit the filter; the KPI route filters only its P&L-side inputs (income statement, months, expense composition) — never cash/VAT. New report lib/reports/dimension-pnl.ts — "Resultat per projekt/ kostnadsställe" (Fortnox Resultatrapport projekt): value-as-column matrix over one dimension with an explicit "(Utan dimension)" bucket computed as the residual against the same trial-balance pass resultatrapport uses, so every row and the Totalt column reconcile with the unfiltered resultatrapport by construction. Registered in REPORT_CATALOG (visible only when dimensions_enabled), slug-routed view + xlsx export. UI: DimensionFilter (dimension + value picker, persistent "Filtrerad — ej fullständig rapport" chip) mounts in FocusedReport for catalog entries flagged dimensions: true; huvudbok rows show line dim codes. Statutory exclusion pinned by TEST, not convention: lib/reports/__tests__/dimension-statutory-guard.test.ts fails if the filter parser leaks into balance sheet, balansrapport, kassaflöde, VAT, SIE or full-archive routes/generators, or if the catalog whitelist widens. MCP: new gnubok_get_dimension_pnl (reports:read); dimensions filter arg on get_trial_balance/get_income_statement/get_general_ledger with resolve-don't-select (names → registry codes, resolution echoes); query_journal totals fixed to aggregate the FULL match set (was silently slice-scoped while claiming otherwise) with an honest totals_scope field, plus group_by / group_by_dimension aggregation. Also: voucher-detail dim-6 badge now uses the registry name instead of the non-standard "PR" abbreviation (#859 review follow-up). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(dimensions): address #862 review — export disclosure, prior-column suppression, period-label honesty, route hardening - Filtered XLSX/PDF exports now carry the partial-view disclosure past the file boundary (BFNAR 2013:2): filename suffix (-dim6-p001), a "Filtrerad … — ej fullständig rapport" row on every sheet, and a header note/title line in the PDFs. - Resultatrapport drops the prior-year column when a dimension filter is active — project codes are time-limited under K2/K3, so "this code last year" may be a different project (same rule as narrowed date ranges). - dimension-pnl no longer accepts fromDate: the matrix is cumulative from period_start by design (closing-balance semantics), and the period label now states exactly that instead of echoing a lower bound that was never applied. Routes/MCP tool updated to toDate-only. - dimension-pnl routes 404 on an unknown/foreign period id and cap dim_no to 4 digits (matching the MCP tool's PostgREST-path guard, which the generator now also enforces itself). - Statutory-guard test's generateTrialBalance call-site scan is paren-aware instead of a 300-char window; added fully-untagged and injection-guard test cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
74 lines
2.3 KiB
TypeScript
74 lines
2.3 KiB
TypeScript
'use client'
|
|
|
|
import { useTranslations } from 'next-intl'
|
|
import { ChevronRight } from 'lucide-react'
|
|
import {
|
|
DataList,
|
|
DataListMeta,
|
|
DataListPrimary,
|
|
DataListRow,
|
|
} from '@/components/ui/data-list'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { getLibrarySections, type ReportDescriptor } from '@/lib/reports/catalog'
|
|
import type { EntityType } from '@/types'
|
|
|
|
/**
|
|
* The report library — the calm landing for /reports. Reports grouped by
|
|
* accounting taxonomy, each section a single DataList. One report = one row =
|
|
* one destination; no data preview, so the landing stays a fast index.
|
|
*/
|
|
export function ReportLibrary({
|
|
entityType,
|
|
hasEmployees,
|
|
dimensionsEnabled,
|
|
onOpen,
|
|
}: {
|
|
entityType?: EntityType
|
|
hasEmployees?: boolean
|
|
dimensionsEnabled?: boolean
|
|
onOpen: (slug: string) => void
|
|
}) {
|
|
const t = useTranslations('reports')
|
|
const sections = getLibrarySections(entityType, hasEmployees, dimensionsEnabled)
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{sections.map((section) => (
|
|
<div key={section.category} className="space-y-3">
|
|
<h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">
|
|
{t(section.labelKey)}
|
|
</h2>
|
|
<DataList>
|
|
{section.items.map((item) => (
|
|
<DataListRow
|
|
key={item.slug}
|
|
onClick={() => onOpen(item.slug)}
|
|
trailing={
|
|
<>
|
|
<EntityBadge item={item} />
|
|
{item.params === 'calendar' && (
|
|
<Badge variant="secondary">{t('calendar_badge')}</Badge>
|
|
)}
|
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
</>
|
|
}
|
|
>
|
|
<DataListPrimary>{t(item.labelKey)}</DataListPrimary>
|
|
<DataListMeta>{t(item.descKey)}</DataListMeta>
|
|
</DataListRow>
|
|
))}
|
|
</DataList>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function EntityBadge({ item }: { item: ReportDescriptor }) {
|
|
if (item.entityType === 'enskild_firma')
|
|
return <span className="text-xs text-muted-foreground">EF</span>
|
|
if (item.entityType === 'aktiebolag')
|
|
return <span className="text-xs text-muted-foreground">AB</span>
|
|
return null
|
|
}
|