Files
accounted/app/(dashboard)/reports/page.tsx
T
Jakob WennbergandClaude Opus 4.8 ff01640f60 feat(reports,settings): report library + focused report routes, settings modal (#629)
* 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>
2026-06-02 14:50:50 +02:00

95 lines
3.2 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { Skeleton } from '@/components/ui/skeleton'
import { Card, CardContent } from '@/components/ui/card'
import { PageHeader } from '@/components/ui/page-header'
import { EmptyState } from '@/components/ui/empty-state'
import { useCompany } from '@/contexts/CompanyContext'
import { FiscalYearSelector } from '@/components/common/FiscalYearSelector'
import { ReportLibrary } from '@/components/reports/ReportLibrary'
import { RecentReportsShelf } from '@/components/reports/RecentReportsShelf'
import { useRecentReports } from '@/components/reports/useRecentReports'
import { getReport } from '@/lib/reports/catalog'
/**
* Reports library landing. A calm, grouped index of every report — selecting
* one opens the focused /reports/[slug] route. The fiscal year picked here
* persists (FiscalYearSelector localStorage) and is restored on the focused
* page, so the choice carries across without URL plumbing.
*/
export default function ReportsPage() {
const router = useRouter()
const [selectedPeriod, setSelectedPeriod] = useState('')
const [isLoadingInit, setIsLoadingInit] = useState(true)
const { company } = useCompany()
const t = useTranslations('reports')
const { recents, pushRecent } = useRecentReports(company?.id)
// Open a report. Route-owning reports (cash flow, annual report, KPI, SIE)
// navigate to their own page; the rest open the focused /reports/[slug] route.
const openReport = (slug: string) => {
const report = getReport(slug)
if (report?.route) {
const href =
slug === 'arsredovisning' && selectedPeriod
? `${report.route}?period=${selectedPeriod}`
: report.route
router.push(href)
return
}
pushRecent(slug)
router.push(`/reports/${slug}`)
}
return (
<div className="space-y-8">
<PageHeader
title={t('title')}
action={
<FiscalYearSelector
value={selectedPeriod || null}
onChange={(id) => setSelectedPeriod(id || '')}
includeAllOption={false}
hideFuturePeriods
onReady={() => setIsLoadingInit(false)}
/>
}
/>
{isLoadingInit ? (
<div className="space-y-6">
<Skeleton className="h-4 w-40" />
<Card>
<CardContent className="p-6 space-y-4">
<Skeleton className="h-5 w-32" />
<Skeleton className="h-64" />
</CardContent>
</Card>
</div>
) : !selectedPeriod ? (
<EmptyState
title="Inget räkenskapsår valt"
description="Skapa ett räkenskapsår för att kunna se rapporter."
actionLabel="Gå till inställningar"
actionHref="/settings"
/>
) : (
<div className="space-y-8">
<RecentReportsShelf
slugs={recents}
entityType={company?.entity_type}
onOpen={openReport}
/>
<ReportLibrary
entityType={company?.entity_type}
onOpen={openReport}
/>
</div>
)}
</div>
)
}