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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f6ee0c2a82
commit
ff01640f60
@@ -0,0 +1,113 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { useSettingsNavItems } from './useSettingsNavItems'
|
||||
|
||||
interface SettingsRailProps {
|
||||
/** Layout context: 'page' navigates with push (real route), 'modal' replaces
|
||||
* the URL so section-switching keeps a single back-stack entry. */
|
||||
variant: 'page' | 'modal'
|
||||
/** 'rail' = grouped vertical list (desktop); 'select' = grouped dropdown (mobile). */
|
||||
display: 'rail' | 'select'
|
||||
/** Explicit active section id. Falls back to the current pathname when omitted
|
||||
* (used by the page variant where the URL is the source of truth). */
|
||||
activeId?: string
|
||||
}
|
||||
|
||||
export function SettingsRail({ variant, display, activeId }: SettingsRailProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const t = useTranslations('settings_nav')
|
||||
const { items, groups } = useSettingsNavItems()
|
||||
|
||||
const resolvedActiveId =
|
||||
activeId ??
|
||||
items.find((i) => pathname.startsWith(i.href))?.id ??
|
||||
items[0]?.id
|
||||
|
||||
function navigate(href: string) {
|
||||
if (variant === 'modal') router.replace(href)
|
||||
else router.push(href)
|
||||
}
|
||||
|
||||
if (display === 'select') {
|
||||
const activeHref =
|
||||
items.find((i) => i.id === resolvedActiveId)?.href ?? items[0]?.href
|
||||
return (
|
||||
<Select value={activeHref} onValueChange={navigate}>
|
||||
<SelectTrigger className="w-full" aria-label={t('aria_label')}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{groups.map((g) => (
|
||||
<SelectGroup key={g.key}>
|
||||
<SelectLabel>{g.label}</SelectLabel>
|
||||
{g.items.map((i) => (
|
||||
<SelectItem key={i.id} value={i.href}>
|
||||
{i.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav aria-label={t('aria_label')} className="space-y-5">
|
||||
{groups.map((g) => (
|
||||
<div key={g.key} className="space-y-1">
|
||||
<p className="px-3 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{g.label}
|
||||
</p>
|
||||
<ul className="space-y-0.5">
|
||||
{g.items.map((i) => {
|
||||
const isActive = i.id === resolvedActiveId
|
||||
const rowClass = cn(
|
||||
'flex min-h-10 items-center rounded-lg px-3 py-2 text-sm transition-colors duration-150',
|
||||
isActive
|
||||
? 'bg-secondary font-medium text-foreground'
|
||||
: 'text-muted-foreground hover:bg-secondary/60 hover:text-foreground',
|
||||
)
|
||||
return (
|
||||
<li key={i.id}>
|
||||
{variant === 'modal' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(i.href)}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(rowClass, 'w-full text-left')}
|
||||
>
|
||||
{i.label}
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href={i.href}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={rowClass}
|
||||
>
|
||||
{i.label}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user