'use client' import { useEffect, useState } from 'react' import { useTranslations } from 'next-intl' import { useCompany } from '@/contexts/CompanyContext' import { Button } from '@/components/ui/button' import { useToast } from '@/components/ui/use-toast' import { DestructiveConfirmDialog, useDestructiveConfirm, } from '@/components/ui/destructive-confirm-dialog' import { Loader2, Lock } from 'lucide-react' import { parseDateParts } from '@/lib/bookkeeping/validate-period-duration' import { validateFirstPeriod } from '@/components/bookkeeping/FiscalPeriodDateFields' import { SettingsGroup, SettingsInput, SettingsRow, SettingsRowNote, } from '@/components/settings/SettingsRows' import type { FiscalPeriod } from '@/types' import { getErrorMessage as getUserErrorMessage } from '@/lib/errors/get-error-message' function formatSwedishDate(dateStr: string): string { const months = [ 'januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', 'augusti', 'september', 'oktober', 'november', 'december', ] const { year, month, day } = parseDateParts(dateStr) return `${day} ${months[month - 1]} ${year}` } function isCalendarYear(period: { period_start: string; period_end: string }): boolean { const s = parseDateParts(period.period_start) const e = parseDateParts(period.period_end) return s.month === 1 && s.day === 1 && e.month === 12 && e.day === 31 } export function FiscalPeriodEditor() { const t = useTranslations('settings_company') const { company, role } = useCompany() const { toast } = useToast() const { dialogProps, confirm } = useDestructiveConfirm() const [period, setPeriod] = useState(null) const [postedCount, setPostedCount] = useState(null) const [isLoading, setIsLoading] = useState(true) const [loadError, setLoadError] = useState(null) const [startDate, setStartDate] = useState('') const [endDate, setEndDate] = useState('') const [isSaving, setIsSaving] = useState(false) const isEF = company?.entity_type === 'enskild_firma' const canEdit = role === 'owner' || role === 'admin' useEffect(() => { if (!company) return let cancelled = false async function load() { setIsLoading(true) setLoadError(null) try { const res = await fetch('/api/bookkeeping/fiscal-periods') if (!res.ok) throw new Error(t('fp_load_error_periods')) const { data } = (await res.json()) as { data: FiscalPeriod[] } if (!data || data.length === 0) { if (!cancelled) { setPeriod(null) setIsLoading(false) } return } const sorted = [...data].sort((a, b) => a.period_start.localeCompare(b.period_start)) const first = sorted[0] const countRes = await fetch(`/api/bookkeeping/fiscal-periods/${first.id}/entry-count`) if (!countRes.ok) throw new Error(t('fp_load_error_entry_count')) const { data: countData } = (await countRes.json()) as { data: { posted_count: number } } if (cancelled) return setPeriod(first) setPostedCount(countData.posted_count) setStartDate(first.period_start) setEndDate(first.period_end) } catch (err) { if (!cancelled) { setLoadError(err instanceof Error ? getUserErrorMessage(err) : t('fp_load_error_unknown')) } } finally { if (!cancelled) setIsLoading(false) } } void load() return () => { cancelled = true } }, [company, t]) const validation = validateFirstPeriod( startDate, endDate, company?.entity_type, ) const isBlocked = !!period && (period.locked_at || period.is_closed || (postedCount ?? 0) > 0) const isDirty = period !== null && (startDate !== period.period_start || endDate !== period.period_end) if (!company || !canEdit) return null async function handleSave() { if (!period || !company) return if (!isDirty) return const ok = await confirm({ title: t('fp_confirm_title'), description: t('fp_confirm_description', { oldStart: formatSwedishDate(period.period_start), oldEnd: formatSwedishDate(period.period_end), newStart: formatSwedishDate(startDate), newEnd: formatSwedishDate(endDate), }), confirmLabel: t('fp_confirm_yes'), cancelLabel: t('fp_confirm_cancel'), variant: 'warning', }) if (!ok) return setIsSaving(true) try { const startYear = parseDateParts(startDate).year const endYear = parseDateParts(endDate).year const newName = startYear === endYear ? t('fp_year_label_single', { year: startYear }) : t('fp_year_label_range', { startYear, endYear }) const res = await fetch(`/api/bookkeeping/fiscal-periods/${period.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ period_start: startDate, period_end: endDate, name: newName, }), }) const body = await res.json().catch(() => ({})) if (!res.ok) { throw new Error(body.error || t('fp_update_failed_title')) } setPeriod(body.data as FiscalPeriod) toast({ title: t('fp_updated_title'), description: `${formatSwedishDate(body.data.period_start)}: ${formatSwedishDate(body.data.period_end)}`, }) } catch (err) { toast({ title: t('fp_update_failed_title'), description: err instanceof Error ? getUserErrorMessage(err) : t('fp_try_again'), variant: 'destructive', }) } finally { setIsSaving(false) } } function handleReset() { if (!period) return setStartDate(period.period_start) setEndDate(period.period_end) } return ( <> {isLoading ? (
{t('fp_loading')}
) : loadError ? (

{loadError}

) : !period ? (

{t('fp_none')}

) : isBlocked ? ( ) : ( <> {/* Consequential warning: stays visible as one quiet ochre sentence. */}

{t('fp_warning_title')} {t('fp_warning_body')} {isEF ? t('fp_warning_ef_suffix') : null}

setStartDate(e.target.value)} className="max-w-44 flex-none tabular-nums" /> setEndDate(e.target.value)} className="max-w-44 flex-none tabular-nums" /> {validation.canSummarise && (

{t('fp_summary_title')}:{' '} {t('fp_range', { start: formatSwedishDate(startDate), end: formatSwedishDate(endDate) })} {validation.months !== null && <> ยท {t('fp_months', { count: validation.months })}}

)} {validation.error && (

{validation.error}

)}
)}
) } function BlockedRow({ period, postedCount, }: { period: FiscalPeriod postedCount: number }) { const t = useTranslations('settings_company') const reason = period.locked_at ? t('fp_blocked_reason_locked') : period.is_closed ? t('fp_blocked_reason_closed') : t('fp_blocked_reason_posted', { count: postedCount }) return (

{t('fp_blocked_title')}. {reason}

{t('fp_blocked_explainer')}

} borderless >
) }