'use client' import { useMemo, useState } from 'react' import { useTranslations } from 'next-intl' import { Search, X } from 'lucide-react' import { TH_CLASS, TD_CLASS } from '@/components/ui/dry-table' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { cn, formatDate } from '@/lib/utils' import { getLibrarySections, reportMatchesQuery, type ReportDescriptor, } from '@/lib/reports/catalog' import type { EntityType } from '@/types' /** * The report catalog as one dry table (concept "Tabellen"): band rows carry * the accounting taxonomy, each report is a single clickable line with its * description in muted ink and a "Senast öppnad" column fed from the * per-company recents. One report = one row = one destination. */ export function ReportLibrary({ entityType, hasEmployees, dimensionsEnabled, openedAt, onOpen, }: { entityType?: EntityType hasEmployees?: boolean dimensionsEnabled?: boolean /** slug -> epoch ms for the "Senast öppnad" column. */ openedAt: Record onOpen: (slug: string) => void }) { const t = useTranslations('reports') const [query, setQuery] = useState('') const allSections = getLibrarySections(entityType, hasEmployees, dimensionsEnabled) // Matched against the translated name and description plus the descriptor's // synonyms, so the vocabulary someone arrives with ("verifikat per konto") // reaches the report even when we named it something else ("Huvudbok"). const sections = useMemo(() => { if (!query.trim()) return allSections return allSections .map((section) => ({ ...section, items: section.items.filter((item) => reportMatchesQuery( `${t(item.labelKey)} ${t(item.descKey)} ${item.searchTerms ?? ''}`, query, ), ), })) .filter((section) => section.items.length > 0) }, [allSections, query, t]) const lastOpenedLabel = (slug: string): string => { const at = openedAt[slug] if (!at) return '' const days = Math.floor((Date.now() - at) / 86_400_000) if (days === 0) return t('opened_today') if (days === 1) return t('opened_yesterday') return formatDate(new Date(at)) } return (
{sections.length === 0 ? (

{t('search_no_results', { query: query.trim() })}

) : (
{/* Stagger is the entry animation for the library as it loads. It must not re-run per keystroke while filtering, or every widening edit replays a 360ms cascade under the user's eyes. */} {sections.map((section) => ( ))}
{t('col_report')} {t('col_description')} {t('col_last_opened')}
)}
) } function SectionRows({ label, items, lastOpenedLabel, onOpen, }: { label: string items: ReportDescriptor[] lastOpenedLabel: (slug: string) => string onOpen: (slug: string) => void }) { const t = useTranslations('reports') return ( <> {label} {items.map((item) => ( onOpen(item.slug)} > {t(item.labelKey)} {item.params === 'calendar' && ( {t('calendar_badge')} )} {t(item.descKey)} {lastOpenedLabel(item.slug)} ))} ) } function EntityMark({ item }: { item: ReportDescriptor }) { if (item.entityType === 'enskild_firma') return EF if (item.entityType === 'aktiebolag') return AB return null }