'use client' import { useState, useEffect, useMemo } from 'react' import dynamic from 'next/dynamic' import { useRouter, useSearchParams } from 'next/navigation' import { useTranslations } from 'next-intl' import JournalEntryList from '@/components/bookkeeping/JournalEntryList' import { type FormLine } from '@/components/bookkeeping/JournalEntryForm' import type { CopyPrefill } from '@/components/bookkeeping/NewJournalEntryDialog' import { DialogLoadingSkeleton } from '@/components/ui/dialog-loading-skeleton' import { SplitButton } from '@/components/ui/split-button' import { useAgentSheet } from '@/components/agent/AgentSheetProvider' import { useUiState } from '@/lib/hooks/use-ui-state' import { resolveInitialMode } from '@/lib/ui-state/client' import { useToast } from '@/components/ui/use-toast' import { Plus, LayoutTemplate, Sparkles } from 'lucide-react' import { PageHeader } from '@/components/ui/page-header' import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver' import type { JournalEntry, JournalEntryLine } from '@/types' const NewJournalEntryDialog = dynamic( () => import('@/components/bookkeeping/NewJournalEntryDialog'), { loading: DialogLoadingSkeleton }, ) const TemplateBookDialog = dynamic( () => import('@/components/bookkeeping/TemplateBookDialog'), { loading: DialogLoadingSkeleton }, ) // SplitButton modes for "Nytt verifikat" (concept scene 9). The last-used // mode persists per user in ui_state.create_mode.bookkeeping. const CREATE_MODES = ['tomt', 'mall', 'assistent'] as const interface NextVoucher { next: number series: string } const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i export default function BookkeepingPage() { const { toast } = useToast() const router = useRouter() const searchParams = useSearchParams() const copyFromId = useMemo(() => { const raw = searchParams.get('copy_from') return raw && UUID_RE.test(raw) ? raw : null }, [searchParams]) const [refreshKey, setRefreshKey] = useState(0) const [showNewEntry, setShowNewEntry] = useState(false) const [showTemplateDialog, setShowTemplateDialog] = useState(false) const [copyPrefill, setCopyPrefill] = useState(null) const [isLoadingCopy, setIsLoadingCopy] = useState(false) const [nextVoucher, setNextVoucher] = useState(null) const t = useTranslations('bookkeeping') const { openAgentSheet } = useAgentSheet() const { uiState, loaded: uiStateLoaded } = useUiState() // React to copy_from in URL: switch tab, fetch source entry, then clean URL. // useSearchParams keeps this reactive even when navigation happens within the // same route (e.g. clicking the Kopiera button in the expanded list row), // which a one-shot useState initializer wouldn't notice. /* eslint-disable react-hooks/set-state-in-effect -- URL→state sync requires sync setState */ useEffect(() => { if (!copyFromId) return setShowNewEntry(true) setCopyPrefill(null) setIsLoadingCopy(true) fetch(`/api/bookkeeping/journal-entries/${copyFromId}`) .then((res) => res.json()) .then(({ data, error }: { data?: JournalEntry; error?: string }) => { if (error || !data) { toast({ title: t('copy_failed_title'), description: error || t('copy_source_missing'), variant: 'destructive', }) return } const sourceLines = ((data.lines || []) as JournalEntryLine[]) .slice() .sort((a, b) => a.sort_order - b.sort_order) const lines: FormLine[] = sourceLines.map((l) => { const debit = Number(l.debit_amount) || 0 const credit = Number(l.credit_amount) || 0 return { account_number: l.account_number, debit_amount: debit > 0 ? debit.toFixed(2) : '', credit_amount: credit > 0 ? credit.toFixed(2) : '', line_description: l.line_description || '', } }) setCopyPrefill({ sourceId: copyFromId, sourceVoucherLabel: formatVoucher(data), lines, description: data.description || '', notes: data.notes || '', }) }) .catch(() => { toast({ title: t('copy_failed_title'), description: t('copy_fetch_failed'), variant: 'destructive', }) }) .finally(() => { setIsLoadingCopy(false) // Clear copy_from so a refresh doesn't re-trigger and so clicking the // same entry's Kopiera button again re-fires this effect. router.replace('/bookkeeping') }) }, [copyFromId, toast, router]) /* eslint-enable react-hooks/set-state-in-effect */ // Fetch the next voucher number for today's fiscal period + default series. // Re-runs after each commit (refreshKey++) so the tab label stays current. useEffect(() => { let cancelled = false fetch('/api/bookkeeping/voucher-sequences/next') .then((r) => r.json()) .then(({ data }) => { if (cancelled) return if (data?.next != null) { setNextVoucher({ next: data.next, series: data.series }) } else { setNextVoucher(null) } }) .catch(() => { if (!cancelled) setNextVoucher(null) }) return () => { cancelled = true } }, [refreshKey]) return (
{ setCopyPrefill(null) setShowNewEntry(true) }, }, { key: 'mall', label: t('create_mall'), icon: LayoutTemplate, description: t('create_mall_desc'), onSelect: () => setShowTemplateDialog(true), }, { key: 'assistent', label: t('create_with_assistant'), icon: Sparkles, description: t('create_assistent_desc'), onSelect: () => openAgentSheet({ intentId: 'verifikation.draft', contextRef: 'verifikation:new', }), }, ]} /> } /> {showTemplateDialog && ( setRefreshKey((k) => k + 1)} /> )} {showNewEntry && ( { setShowNewEntry(o) if (!o) setCopyPrefill(null) }} onCreated={() => { setRefreshKey((k) => k + 1) setShowNewEntry(false) setCopyPrefill(null) }} copyPrefill={copyPrefill} isLoading={isLoadingCopy} /> )}
) }