ff01640f60
* feat(reports,settings): report library + focused report routes, settings modal Reports - Replace the monolithic /reports tab-switcher with a calm, grouped report library landing (ReportLibrary + RecentReportsShelf) driven by a new lib/reports/catalog.ts. - Each report opens a focused /reports/[slug] route (FocusedReport) with a shared fiscal-year selector, optional date-range, and URL-based account drill-down into the general ledger. - Extract every report view into components/reports/views, add a reusable ReportExportMenu, and remove the old ReportsNav. Settings - Add an intercepting @settingsModal parallel route so in-app navigation to /settings opens as a modal over the current page; hard loads still resolve to the full page. - Share one SettingsShell (rail + content) between page and modal, extract each section into components/settings/sections/*Content, add a settings hotkey and command-palette entry, and remove the old SettingsSidebar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reports,settings): remove dead salary-journal entry, fix border token Addresses PR review feedback (#629): - Remove the unreachable `salary-journal` report from the catalog. It had needsEmployees + no route + no FocusedView handler and `hasEmployees` was never plumbed through, so it never appeared in the library and a direct /reports/salary-journal URL rendered a blank frame. The report was never on the old page and has no view component; the API + generator stay in place for a proper follow-up. Drops its two now-unused i18n keys. - Replace opacity-suffixed `border-border/8` section dividers with full-opacity `border-border` across the extracted settings section components, per the design system (no opacity-suffixed border tokens on surfaces). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: re-trigger checks (pg-real hit a Docker Hub registry timeout) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
303 lines
8.0 KiB
TypeScript
303 lines
8.0 KiB
TypeScript
import type { EntityType } from '@/types'
|
|
|
|
/**
|
|
* Single source of truth for the reports surface.
|
|
*
|
|
* One descriptor per report drives every entry point: the report-library
|
|
* landing (`ReportLibrary`), the "Senast öppnade" recent shelf, the focused
|
|
* report route (`/reports/[slug]` via `FocusedReport`), and the command-palette
|
|
* "Visa rapport" jumps. Adding a report = adding one row here.
|
|
*
|
|
* `labelKey` / `descKey` resolve against the `reports` i18n namespace. The
|
|
* category labels reuse the existing `group_*` keys so statutory terminology is
|
|
* never re-translated.
|
|
*/
|
|
|
|
export type ReportCategory =
|
|
| 'interim'
|
|
| 'year_end'
|
|
| 'tax_vat'
|
|
| 'ledgers'
|
|
| 'reconciliation'
|
|
| 'payroll'
|
|
| 'export'
|
|
|
|
/**
|
|
* How the report is parameterised:
|
|
* - `fiscal-range`: fiscal period + an optional date sub-range (ReportDateRange)
|
|
* - `fiscal`: fiscal period only
|
|
* - `calendar`: calendar year + monthly/quarterly/yearly period (VAT family) —
|
|
* the deliberate exception to "pick the fiscal year once"
|
|
* - `none`: no period parameter
|
|
*/
|
|
export type ReportParams = 'fiscal-range' | 'fiscal' | 'calendar' | 'none'
|
|
|
|
export type ReportExportFormat = 'pdf' | 'xlsx'
|
|
|
|
export interface ReportDescriptor {
|
|
/** URL slug at /reports/[slug]; also the legacy activeTab id. */
|
|
slug: string
|
|
/** i18n key in the `reports` namespace for the display name. */
|
|
labelKey: string
|
|
/** i18n key in the `reports` namespace for the one-line description. */
|
|
descKey: string
|
|
category: ReportCategory
|
|
/** When set, the report only appears for this entity type. */
|
|
entityType?: EntityType
|
|
/** When true, only shown if the company has employees. */
|
|
needsEmployees?: boolean
|
|
params: ReportParams
|
|
/** On-page export formats handled by the focused view's export menu. */
|
|
exports?: ReportExportFormat[]
|
|
/**
|
|
* External destination. When set, the library/nav links straight here instead
|
|
* of /reports/[slug] (e.g. reports that own their own route, or live elsewhere).
|
|
*/
|
|
route?: string
|
|
/**
|
|
* Hidden from the legacy desktop rail; surfaced only on the library landing.
|
|
* Used for reports that were never in the nav (KPI, payroll, archive…).
|
|
*/
|
|
libraryOnly?: boolean
|
|
}
|
|
|
|
/** Categories shown in the legacy desktop rail, in order. */
|
|
export const NAV_CATEGORIES: ReportCategory[] = [
|
|
'interim',
|
|
'year_end',
|
|
'tax_vat',
|
|
'ledgers',
|
|
'reconciliation',
|
|
]
|
|
|
|
/** All categories shown on the library landing, in order. */
|
|
export const LIBRARY_CATEGORIES: ReportCategory[] = [
|
|
'interim',
|
|
'year_end',
|
|
'tax_vat',
|
|
'ledgers',
|
|
'reconciliation',
|
|
'payroll',
|
|
'export',
|
|
]
|
|
|
|
/** Maps a category to its existing `group_*` i18n label key. */
|
|
export const CATEGORY_LABEL_KEY: Record<ReportCategory, string> = {
|
|
interim: 'group_interim',
|
|
year_end: 'group_year_end',
|
|
tax_vat: 'group_tax_vat',
|
|
ledgers: 'group_ledgers',
|
|
reconciliation: 'group_reconciliation',
|
|
payroll: 'group_payroll',
|
|
export: 'group_export',
|
|
}
|
|
|
|
export const REPORT_CATALOG: ReportDescriptor[] = [
|
|
// --- Löpande (interim) ---
|
|
{
|
|
slug: 'resultatrapport',
|
|
labelKey: 'name_resultatrapport',
|
|
descKey: 'desc_resultatrapport',
|
|
category: 'interim',
|
|
params: 'fiscal-range',
|
|
exports: ['pdf', 'xlsx'],
|
|
},
|
|
{
|
|
slug: 'balansrapport',
|
|
labelKey: 'name_balansrapport',
|
|
descKey: 'desc_balansrapport',
|
|
category: 'interim',
|
|
params: 'fiscal-range',
|
|
exports: ['pdf', 'xlsx'],
|
|
},
|
|
{
|
|
slug: 'trial-balance',
|
|
labelKey: 'name_trial_balance',
|
|
descKey: 'desc_trial_balance',
|
|
category: 'interim',
|
|
params: 'fiscal',
|
|
exports: ['xlsx'],
|
|
},
|
|
{
|
|
slug: 'kpi',
|
|
labelKey: 'name_kpi',
|
|
descKey: 'desc_kpi',
|
|
category: 'interim',
|
|
params: 'fiscal',
|
|
route: '/kpi',
|
|
libraryOnly: true,
|
|
},
|
|
|
|
// --- Bokslut (year-end) ---
|
|
{
|
|
slug: 'income-statement',
|
|
labelKey: 'name_income_statement',
|
|
descKey: 'desc_income_statement',
|
|
category: 'year_end',
|
|
params: 'fiscal-range',
|
|
exports: ['pdf', 'xlsx'],
|
|
},
|
|
{
|
|
slug: 'balance-sheet',
|
|
labelKey: 'name_balance_sheet',
|
|
descKey: 'desc_balance_sheet',
|
|
category: 'year_end',
|
|
params: 'fiscal-range',
|
|
exports: ['pdf', 'xlsx'],
|
|
},
|
|
{
|
|
slug: 'kassaflodesanalys',
|
|
labelKey: 'name_kassaflodesanalys',
|
|
descKey: 'desc_kassaflodesanalys',
|
|
category: 'year_end',
|
|
params: 'fiscal',
|
|
route: '/reports/kassaflodesanalys',
|
|
},
|
|
{
|
|
slug: 'arsredovisning',
|
|
labelKey: 'name_arsredovisning',
|
|
descKey: 'desc_arsredovisning',
|
|
category: 'year_end',
|
|
entityType: 'aktiebolag',
|
|
params: 'fiscal',
|
|
route: '/bookkeeping/year-end/arsredovisning',
|
|
},
|
|
|
|
// --- Skatt & moms (tax & VAT) ---
|
|
{
|
|
slug: 'vat-declaration',
|
|
labelKey: 'name_vat_declaration',
|
|
descKey: 'desc_vat_declaration',
|
|
category: 'tax_vat',
|
|
params: 'calendar',
|
|
exports: ['xlsx'],
|
|
},
|
|
{
|
|
slug: 'periodisk-sammanstallning',
|
|
labelKey: 'name_periodisk_sammanstallning',
|
|
descKey: 'desc_periodisk_sammanstallning',
|
|
category: 'tax_vat',
|
|
params: 'calendar',
|
|
},
|
|
{
|
|
slug: 'ne-declaration',
|
|
labelKey: 'name_ne_declaration',
|
|
descKey: 'desc_ne_declaration',
|
|
category: 'tax_vat',
|
|
entityType: 'enskild_firma',
|
|
params: 'fiscal',
|
|
},
|
|
{
|
|
slug: 'ink2-declaration',
|
|
labelKey: 'name_ink2_declaration',
|
|
descKey: 'desc_ink2_declaration',
|
|
category: 'tax_vat',
|
|
entityType: 'aktiebolag',
|
|
params: 'fiscal',
|
|
},
|
|
|
|
// --- Huvudböcker (ledgers) ---
|
|
{
|
|
slug: 'huvudbok',
|
|
labelKey: 'name_huvudbok',
|
|
descKey: 'desc_huvudbok',
|
|
category: 'ledgers',
|
|
params: 'fiscal',
|
|
exports: ['xlsx'],
|
|
},
|
|
{
|
|
slug: 'grundbok',
|
|
labelKey: 'name_grundbok',
|
|
descKey: 'desc_grundbok',
|
|
category: 'ledgers',
|
|
params: 'fiscal',
|
|
exports: ['xlsx'],
|
|
},
|
|
{
|
|
slug: 'kundreskontra',
|
|
labelKey: 'name_kundreskontra',
|
|
descKey: 'desc_kundreskontra',
|
|
category: 'ledgers',
|
|
params: 'fiscal',
|
|
exports: ['xlsx'],
|
|
},
|
|
{
|
|
slug: 'supplier-ledger',
|
|
labelKey: 'name_supplier_ledger',
|
|
descKey: 'desc_supplier_ledger',
|
|
category: 'ledgers',
|
|
params: 'fiscal',
|
|
exports: ['xlsx'],
|
|
},
|
|
|
|
// --- Avstämning (reconciliation) ---
|
|
{
|
|
slug: 'bank-reconciliation',
|
|
labelKey: 'name_bank_reconciliation',
|
|
descKey: 'desc_bank_reconciliation',
|
|
category: 'reconciliation',
|
|
params: 'none',
|
|
},
|
|
|
|
// --- Export & arkiv — library-only ---
|
|
{
|
|
slug: 'sie-export',
|
|
labelKey: 'name_sie_export',
|
|
descKey: 'desc_sie_export',
|
|
category: 'export',
|
|
params: 'fiscal',
|
|
route: '/import?view=export#sie-export',
|
|
libraryOnly: true,
|
|
},
|
|
]
|
|
|
|
/** Reports that take a fiscal period + optional date sub-range. */
|
|
export const DATE_RANGE_SLUGS: ReadonlySet<string> = new Set(
|
|
REPORT_CATALOG.filter((r) => r.params === 'fiscal-range').map((r) => r.slug),
|
|
)
|
|
|
|
export function getReport(slug: string): ReportDescriptor | undefined {
|
|
return REPORT_CATALOG.find((r) => r.slug === slug)
|
|
}
|
|
|
|
function isVisible(
|
|
r: ReportDescriptor,
|
|
entityType?: EntityType,
|
|
hasEmployees?: boolean,
|
|
): boolean {
|
|
if (r.entityType && r.entityType !== entityType) return false
|
|
if (r.needsEmployees && !hasEmployees) return false
|
|
return true
|
|
}
|
|
|
|
export interface ReportSection {
|
|
category: ReportCategory
|
|
labelKey: string
|
|
items: ReportDescriptor[]
|
|
}
|
|
|
|
/** Grouped reports for the legacy desktop rail (excludes library-only items). */
|
|
export function getNavSections(entityType?: EntityType): ReportSection[] {
|
|
return NAV_CATEGORIES.map((category) => ({
|
|
category,
|
|
labelKey: CATEGORY_LABEL_KEY[category],
|
|
items: REPORT_CATALOG.filter(
|
|
(r) => r.category === category && !r.libraryOnly && isVisible(r, entityType),
|
|
),
|
|
})).filter((s) => s.items.length > 0)
|
|
}
|
|
|
|
/** Grouped reports for the library landing (includes everything visible). */
|
|
export function getLibrarySections(
|
|
entityType?: EntityType,
|
|
hasEmployees?: boolean,
|
|
): ReportSection[] {
|
|
return LIBRARY_CATEGORIES.map((category) => ({
|
|
category,
|
|
labelKey: CATEGORY_LABEL_KEY[category],
|
|
items: REPORT_CATALOG.filter(
|
|
(r) => r.category === category && isVisible(r, entityType, hasEmployees),
|
|
),
|
|
})).filter((s) => s.items.length > 0)
|
|
}
|