'use client' import React, { useState, useCallback, useMemo } from 'react' import Link from 'next/link' import { ChevronDown, ChevronRight, AlertCircle } from 'lucide-react' import { Skeleton } from '@/components/ui/skeleton' import { formatAmount, formatDate } from '@/lib/utils' import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver' import { createSourceLoader, type ReportSourceLine, type ReportSourceFetcher, } from '@/lib/reports/source-lines' interface ReportRowExpansionProps { /** Lazy fetcher invoked the first time the row is expanded. */ fetcher: ReportSourceFetcher /** Column span used by the inline expansion ``. */ colSpan: number /** Stable id used as the toggle's aria-controls reference. */ rowId: string } interface UseSourceLinesResult { lines: ReportSourceLine[] | null loading: boolean error: string | null load: () => Promise } /** * Hook owning the lazy fetch + caching of source lines. Thin wrapper on top * of `createSourceLoader` from `lib/reports/source-lines` so the loading * semantics can be unit-tested in node without DOM. * * The cache lives per-instance: reopening the same row never refetches. */ export function useSourceLines(fetcher: ReportSourceFetcher): UseSourceLinesResult { const [lines, setLines] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) // The loader is stable for the lifetime of the fetcher reference; callers // are expected to memoise their fetcher with `useMemo` (every callsite in // `reports/page.tsx` does). const loader = useMemo( () => createSourceLoader(fetcher, (s) => { setLines(s.lines) setLoading(s.loading) setError(s.error) }), [fetcher] ) const load = useCallback(() => loader.load(), [loader]) return { lines, loading, error, load } } /** * Drilldown affordance for aggregated report rows. * * Renders two cells (the chevron-toggle is placed in the calling row; the * expansion itself is rendered as a sibling `` only when expanded). * The caller is responsible for placing `` inside * the aggregated row and `` immediately below. * * Usage: * const expansion = useReportRowExpansion(fetcher) * ... * {expansion.expanded && } * * Or use the all-in-one component below where the caller owns the * surrounding `` and just plugs the expansion in. */ export function ReportRowExpansion({ fetcher, colSpan, rowId, }: ReportRowExpansionProps) { const [expanded, setExpanded] = useState(false) const { lines, loading, error, load } = useSourceLines(fetcher) const toggle = useCallback(() => { const next = !expanded setExpanded(next) if (next) load() }, [expanded, load]) return ( <> {expanded && ( )} ) } /** * Variant where the toggle and the expansion are placed by the caller. The * caller renders `` inside the aggregated ``, then below * conditionally renders `` as a sibling `` so its `` lines up with the rest of the table. */ export function useReportRowExpansion(fetcher: ReportSourceFetcher, rowId: string) { const [expanded, setExpanded] = useState(false) const { lines, loading, error, load } = useSourceLines(fetcher) const toggle = useCallback(() => { const next = !expanded setExpanded(next) if (next) load() }, [expanded, load]) const Toggle = () => ( ) const Panel = ({ colSpan }: { colSpan: number }) => expanded ? ( ) : null return { expanded, Toggle, Panel } } /** * Body of the expansion when the wrapping `` colSpan is provided: * renders inline (used by ReportRowExpansion). */ function ExpansionPanel({ colSpan, loading, error, lines, rowId, }: { colSpan: number loading: boolean error: string | null lines: ReportSourceLine[] | null rowId: string }) { return ( ) } /** * Body of the expansion when used in a sibling `` (used by * `useReportRowExpansion`'s Panel). */ function ExpansionPanelRow({ colSpan, loading, error, lines, rowId, }: { colSpan: number loading: boolean error: string | null lines: ReportSourceLine[] | null rowId: string }) { return ( ) } function ExpansionContent({ loading, error, lines, }: { loading: boolean error: string | null lines: ReportSourceLine[] | null }) { if (loading) { return (
) } if (error) { return (
{error}
) } if (!lines || lines.length === 0) { return (
Inga underliggande verifikat.
) } return (
{lines.map((line) => ( ))}
Verifikat Datum Beskrivning Debet Kredit
{line.journal_entry_id ? ( {formatVoucher(line)} ) : ( - )} {line.date ? formatDate(line.date) : ''} {line.description} {line.debit > 0 ? formatAmount(line.debit) : ''} {line.credit > 0 ? formatAmount(line.credit) : ''}
) }