'use client' import { Fragment, useState } from 'react' import Image from 'next/image' import { useTranslations } from 'next-intl' import { ChevronDown, ChevronRight } from 'lucide-react' import { cn, formatDate } from '@/lib/utils' import type { ReconciliationAccount } from '@/lib/reconciliation/schemas' /** * The account rail of the Avstämning page: one row per account with an * outside truth (bank accounts, the skattekonto), logo or monogram, the * account number, when its outside side was last fetched, and a status dot. * The rest of the balance sheet follows under its own label: those accounts * have no feed, so their row says whether they are signed off instead. * Selection is URL-owned (?account=) by the workspace; the rail only reports. */ const DOT_CLASS: Record['state'] | 'unknown', string> = { reconciled: 'bg-success', open: 'bg-warning', stale: 'bg-warning', not_configured: 'bg-muted-foreground/40', unknown: 'bg-muted-foreground/40', } function monogram(name: string): string { const words = name.trim().split(/\s+/).filter(Boolean) if (words.length === 0) return '?' if (words.length === 1) return words[0].slice(0, 2).toUpperCase() return (words[0][0] + words[1][0]).toUpperCase() } export function AccountLogo({ account, className }: { account: ReconciliationAccount; className?: string }) { if (account.logo_url) { return ( ) } const label = account.kind === 'manual' ? account.account_number.slice(0, 2) : monogram(account.name) return ( {label} ) } interface ReconciliationRailProps { accounts: ReconciliationAccount[] selectedKey: string | null onSelect: (accountKey: string) => void /** Rendered under the list: the way to the pärm the accounts feed. */ footer?: React.ReactNode } const MANUAL_OPEN_STORAGE_KEY = 'Accounted:recon-rail-manual-open' export function ReconciliationRail({ accounts, selectedKey, onSelect, footer }: ReconciliationRailProps) { const t = useTranslations('reconciliation') const fed = accounts.filter((a) => a.kind !== 'manual') const manual = accounts.filter((a) => a.kind === 'manual') const selectedIsManual = selectedKey?.startsWith('manual:') ?? false // The manual group folds: a migrated company has twenty-odd balance // accounts, and the bank rows must stay in view. It opens when a manual // account is selected and otherwise remembers the last choice per browser. const [manualOpen, setManualOpen] = useState(() => { if (selectedIsManual) return true try { return window.localStorage.getItem(MANUAL_OPEN_STORAGE_KEY) === '1' } catch { return false } }) // Derived, not synced: the selected row must stay visible. const showManual = manualOpen || selectedIsManual const toggleManual = () => { setManualOpen((open) => { try { window.localStorage.setItem(MANUAL_OPEN_STORAGE_KEY, open ? '0' : '1') } catch { // Per-browser convenience only. } return !open }) } const manualUnsigned = manual.filter((a) => a.status?.state !== 'reconciled').length const stateLabel = (account: ReconciliationAccount): string => { const state = account.status?.state ?? 'unknown' if (account.kind === 'manual' && state === 'open') return t('state_unsigned') return t(`state_${state === 'unknown' ? 'not_configured' : state}`) } const subLine = (account: ReconciliationAccount): string => { if (account.signed_off_through) return t('rail_signed_off', { date: formatDate(account.signed_off_through) }) if (account.kind === 'manual') return t('rail_never_signed') const synced = account.source.synced_at return synced ? t('rail_synced', { date: formatDate(synced) }) : t('rail_never_synced') } // A manual account with nothing to compare against is not a problem, just // not attested yet: neutral until it is signed or a specification differs. const dotState = (account: ReconciliationAccount): keyof typeof DOT_CLASS => { const state = account.status?.state ?? 'unknown' if (account.kind === 'manual' && state === 'open' && account.status?.unexplained_difference == null) return 'unknown' return state } const renderRow = (account: ReconciliationAccount) => { const selected = account.account_key === selectedKey const state = dotState(account) const open = account.status ? account.status.open_counts.proposed + account.status.open_counts.unmatched_external + account.status.open_counts.unmatched_ledger : 0 return (
  • ) } return ( ) }