'use client' import { useCallback, useEffect, useMemo, useState } from 'react' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useTranslations } from 'next-intl' import { Scale } from 'lucide-react' import { PageHeader } from '@/components/ui/page-header' import { HelpPopover } from '@/components/ui/help-popover' import { EmptyState } from '@/components/ui/empty-state' import { AttnLine } from '@/components/ui/attn-line' import { Skeleton } from '@/components/ui/skeleton' import { FyPicker } from '@/components/common/FyPicker' import { ReportDateRange, type DateRangeValue } from '@/components/common/ReportDateRange' import type { ReconciliationAccount } from '@/lib/reconciliation/schemas' import type { FiscalPeriod } from '@/types' import { ReconciliationRail } from './ReconciliationRail' import { AccountOverview, type ReconciliationWindow } from './AccountOverview' /** * /reconciliation: one page for every account with an outside truth. The * rail on the left lists the accounts (bank accounts, the skattekonto) with * their status; the body shows the selected account's bridge and the rows * behind it. Selection lives in the URL (?account=) so a link lands on the * right account and a reload keeps it. * * The period (räkenskapsår + range within it) scopes the bank bridge and the * item windows and sets the default sign-off date. It keeps its own preset * memory, separate from the reports: reconciling is a monthly ritual, so it * opens on this month rather than on whatever range a report left behind. */ const FY_STORAGE_KEY_PREFIX = 'Accounted:recon-fy:' const RANGE_STORAGE_KEY_PREFIX = 'Accounted:recon-page-range-preset:' interface ReconciliationWorkspaceProps { initialPeriods: FiscalPeriod[] initialCompanyId: string | null } export function ReconciliationWorkspace({ initialPeriods, initialCompanyId }: ReconciliationWorkspaceProps) { const t = useTranslations('reconciliation') const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() const [accounts, setAccounts] = useState(null) const [loadError, setLoadError] = useState(false) const [periodId, setPeriodId] = useState(null) const [periodBounds, setPeriodBounds] = useState<{ start: string; end: string } | null>(null) const [dateRange, setDateRange] = useState({}) // The effective window: the range within the period, defaulting to the // period bounds. Null until the period picker has resolved. const window = useMemo(() => { if (!periodBounds) return null return { from: dateRange.fromDate ?? periodBounds.start, to: dateRange.toDate ?? periodBounds.end, } }, [periodBounds, dateRange]) const load = useCallback(async () => { if (!window) return try { const qs = new URLSearchParams({ date_from: window.from, date_to: window.to }) const res = await fetch(`/api/reconciliation/accounts?${qs.toString()}`) setLoadError(false) if (!res.ok) { setLoadError(true) return } const json = await res.json() setAccounts((json.data?.accounts ?? []) as ReconciliationAccount[]) } catch { setLoadError(true) } }, [window]) useEffect(() => { void load() }, [load]) const requestedKey = searchParams.get('account') const selected = useMemo(() => { if (!accounts || accounts.length === 0) return null return ( accounts.find((a) => a.account_key === requestedKey) ?? accounts.find((a) => !a.superseded_by) ?? accounts[0] ) }, [accounts, requestedKey]) const select = useCallback( (accountKey: string) => { const params = new URLSearchParams(searchParams.toString()) params.set('account', accountKey) router.replace(`${pathname}?${params.toString()}`, { scroll: false }) }, [pathname, router, searchParams], ) const header = (

{t('help_text')}

} action={
{ setPeriodId(id) setPeriodBounds(period ? { start: period.period_start, end: period.period_end } : null) setDateRange({}) }} includeAllOption={false} hideFuturePeriods initialPeriods={initialPeriods} initialCompanyId={initialCompanyId} storageKeyPrefix={FY_STORAGE_KEY_PREFIX} /> {periodBounds && ( )}
} /> ) if (loadError) { return (
{header} void load() }}>{t('load_failed')}
) } if (accounts === null || !window) { return (
{header}
) } if (accounts.length === 0) { return (
{header}
) } return (
{header} {selected && ( } window={window} onChanged={() => void load()} /> )}
) }