'use client' import { useState, useMemo } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { useToast } from '@/components/ui/use-toast' import { Loader2, Lock } from 'lucide-react' import type { FiscalPeriod } from '@/types' interface Props { open: boolean onOpenChange: (open: boolean) => void entryDate: string periods: FiscalPeriod[] onCreated: () => void } /** A prior period that must be locked before a new fiscal year can be created. */ interface BlockingPeriod { id: string name: string period_start: string period_end: string } /** Read a user-facing message from either a legacy string error or the * canonical { code, message } envelope. */ function errorMessage(err: unknown, fallback = 'Ett oväntat fel uppstod.'): string { if (typeof err === 'string') return err if (err && typeof err === 'object' && typeof (err as { message?: unknown }).message === 'string') { return (err as { message: string }).message } return fallback } function computeSuggestedPeriod(entryDate: string, periods: FiscalPeriod[]) { if (periods.length === 0) { // No periods at all — suggest a calendar year period around the entry date const year = entryDate.split('-')[0] return { name: `FY ${year}`, period_start: `${year}-01-01`, period_end: `${year}-12-31`, } } const sorted = [...periods].sort((a, b) => a.period_start.localeCompare(b.period_start)) const earliest = sorted[0] const latest = sorted[sorted.length - 1] if (entryDate < earliest.period_start) { // Backward: end = day before earliest start, start = 12 months back, 1st of month // Use UTC throughout — local-time Date math + toISOString() shifts dates by // the timezone offset (e.g. CET produces 2024-12-31 → 2025-12-30). const end = new Date(earliest.period_start + 'T00:00:00Z') end.setUTCDate(end.getUTCDate() - 1) const start = new Date(end) start.setUTCMonth(start.getUTCMonth() - 11) start.setUTCDate(1) const startStr = start.toISOString().split('T')[0] const endStr = end.toISOString().split('T')[0] const startYear = start.getUTCFullYear() const endYear = end.getUTCFullYear() const name = startYear === endYear ? `FY ${startYear}` : `FY ${startYear}/${endYear}` return { name, period_start: startStr, period_end: endStr } } // Forward: start = day after latest end, end = 12 months later (last day of month) const start = new Date(latest.period_end + 'T00:00:00Z') start.setUTCDate(start.getUTCDate() + 1) const end = new Date(start) end.setUTCMonth(end.getUTCMonth() + 12) end.setUTCDate(0) // Last day of previous month const startStr = start.toISOString().split('T')[0] const endStr = end.toISOString().split('T')[0] const startYear = start.getUTCFullYear() const endYear = end.getUTCFullYear() const name = startYear === endYear ? `FY ${startYear}` : `FY ${startYear}/${endYear}` return { name, period_start: startStr, period_end: endStr } } export default function CreatePeriodDialog({ open, onOpenChange, entryDate, periods, onCreated }: Props) { const { toast } = useToast() const suggested = useMemo(() => computeSuggestedPeriod(entryDate, periods), [entryDate, periods]) const [name, setName] = useState(suggested.name) const [periodStart, setPeriodStart] = useState(suggested.period_start) const [periodEnd, setPeriodEnd] = useState(suggested.period_end) const [isSubmitting, setIsSubmitting] = useState(false) const [isLocking, setIsLocking] = useState(false) // Set when creation is blocked because a prior räkenskapsår is still open. // The user can lock these inline and retry without leaving the dialog. const [blockingPeriods, setBlockingPeriods] = useState([]) // Reset form when suggested values change (dialog reopened with new date) const [lastSuggested, setLastSuggested] = useState(suggested) if (suggested.name !== lastSuggested.name || suggested.period_start !== lastSuggested.period_start) { setName(suggested.name) setPeriodStart(suggested.period_start) setPeriodEnd(suggested.period_end) setLastSuggested(suggested) setBlockingPeriods([]) } const handleCreate = async () => { setIsSubmitting(true) try { const res = await fetch('/api/bookkeeping/fiscal-periods', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, period_start: periodStart, period_end: periodEnd }), }) const result = await res.json() if (!res.ok) { const err = result?.error // Blocked by an open prior year — surface an inline "lås och försök // igen" path instead of a dead-end toast. if ( err && typeof err === 'object' && err.code === 'PERIOD_CREATE_BLOCKED_BY_OPEN_PERIODS' ) { const blocking = (err.details?.blockingPeriods ?? []) as BlockingPeriod[] setBlockingPeriods(blocking) return } toast({ title: 'Kunde inte skapa räkenskapsår', description: errorMessage(err), variant: 'destructive', }) return } toast({ title: 'Räkenskapsår skapat', description: `${name} har skapats.` }) setBlockingPeriods([]) onOpenChange(false) onCreated() } catch { toast({ title: 'Kunde inte skapa räkenskapsår', description: 'Ett nätverksfel uppstod. Försök igen.', variant: 'destructive', }) } finally { setIsSubmitting(false) } } // Lock each blocking prior year (reversible locked_at), then retry creation. const handleLockAndRetry = async () => { setIsLocking(true) try { for (const p of blockingPeriods) { const res = await fetch(`/api/bookkeeping/fiscal-periods/${p.id}/lock`, { method: 'POST', }) if (!res.ok) { const body = await res.json().catch(() => ({})) // An already-locked period is fine — keep going. if (body?.error?.code === 'PERIOD_LOCK_ALREADY_LOCKED') continue toast({ title: `Kunde inte låsa ${p.name}`, description: errorMessage(body?.error), variant: 'destructive', }) return } } setBlockingPeriods([]) await handleCreate() } catch { toast({ title: 'Kunde inte låsa räkenskapsåret', description: 'Ett nätverksfel uppstod. Försök igen.', variant: 'destructive', }) } finally { setIsLocking(false) } } const isBlocked = blockingPeriods.length > 0 const busy = isSubmitting || isLocking return ( Skapa räkenskapsår Det finns inget räkenskapsår som täcker datumet {entryDate}. Skapa ett nytt nedan.
setName(e.target.value)} className="mt-1" />
setPeriodStart(e.target.value)} className="mt-1" />
setPeriodEnd(e.target.value)} className="mt-1" />
{isBlocked && (

Föregående räkenskapsår är öppet

Du måste låsa föregående räkenskapsår innan du kan skapa ett nytt. Låsningen är vändbar — du kan låsa upp året igen för att bokföra bokslutsposter.

    {blockingPeriods.map((p) => (
  • {p.name} ({p.period_start} – {p.period_end})
  • ))}
)}
{isBlocked ? ( ) : ( )}
) }