diff --git a/app/(dashboard)/bookkeeping/[id]/page.tsx b/app/(dashboard)/bookkeeping/[id]/page.tsx
index 99fe0a60..f5c2d4d0 100644
--- a/app/(dashboard)/bookkeeping/[id]/page.tsx
+++ b/app/(dashboard)/bookkeeping/[id]/page.tsx
@@ -8,12 +8,13 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { AccountNumber } from '@/components/ui/account-number'
import { Textarea } from '@/components/ui/textarea'
-import { Loader2, ArrowLeft, Paperclip, AlertTriangle, Lock, MessageSquare, Pencil, Check, X, Copy, ChevronDown, CalendarClock, FileText, Link2 } from 'lucide-react'
+import { Loader2, ArrowLeft, Paperclip, AlertTriangle, Lock, MessageSquare, Pencil, Check, X, Copy, ChevronDown, CalendarClock, FileText, Link2, RotateCcw } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuSeparator,
} from '@/components/ui/dropdown-menu'
import { useCanWrite } from '@/lib/hooks/use-can-write'
import { formatDate } from '@/lib/utils'
@@ -45,6 +46,8 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
const [showEdit, setShowEdit] = useState(false)
const [showRecordate, setShowRecordate] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
+ const [showReverseConfirm, setShowReverseConfirm] = useState(false)
+ const [isReversing, setIsReversing] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [isCommitting, setIsCommitting] = useState(false)
const [isLastInSeries, setIsLastInSeries] = useState(false)
@@ -155,6 +158,33 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
}
}, [id, router, toast, t])
+ // Pure reversal (storno) — cancels the verifikat with a stornoverifikation and
+ // no replacement, per BFL 5 kap 5§. Distinct from "Rätta", which always books
+ // a replacement entry. Routes through the engine's reverseEntry (storno +
+ // reverses_id link; original → 'reversed', never deleted).
+ const handleReverse = useCallback(async () => {
+ setIsReversing(true)
+ try {
+ const res = await fetch(`/api/bookkeeping/journal-entries/${id}/reverse`, { method: 'POST' })
+ const result = await res.json()
+ if (res.ok) {
+ const storno = result.data
+ toast({
+ title: t('toast_reverse_done_title'),
+ description: t('toast_reverse_done_description', { voucher: formatVoucher(storno ?? {}) }),
+ })
+ setShowReverseConfirm(false)
+ await fetchData()
+ } else {
+ toast({ title: t('toast_reverse_failed'), description: getErrorMessage(result, { context: 'journal_entry' }), variant: 'destructive' })
+ }
+ } catch {
+ toast({ title: t('toast_reverse_failed'), variant: 'destructive' })
+ } finally {
+ setIsReversing(false)
+ }
+ }, [id, toast, fetchData, t])
+
useEffect(() => {
fetchData()
}, [fetchData])
@@ -297,6 +327,11 @@ export default function JournalEntryDetailPage({ params }: { params: Promise<{ i
{t('reverse_dialog_heading', { voucher: formatVoucher(entry) })} {t('reverse_dialog_body')}
Rättade rader
+Rättade rader
++ Det här är hela den nya verifikationen — alla konton som ska finnas kvar måste stå + kvar. Tar du bort ett konto nollställs det (stornon återför det). Vill du bara återföra + hela verifikatet utan att ersätta det, använd Återför (storno) istället. +
+Förändring = storno + rättelse. Det är det netto som tillkommer ovanpå originalet när du - bokför. + bokför. Ett konto du tar bort nollställs av stornon.
+ {unbalanced && ( ++ Förslaget balanserar inte ännu – debet och kredit i rättelsen måste vara lika. +
+ )} ) } diff --git a/components/bookkeeping/JournalEntryList.tsx b/components/bookkeeping/JournalEntryList.tsx index c1365a48..043d2698 100644 --- a/components/bookkeeping/JournalEntryList.tsx +++ b/components/bookkeeping/JournalEntryList.tsx @@ -26,15 +26,17 @@ import { STORAGE_KEY_PREFIX as FISCAL_YEAR_STORAGE_KEY_PREFIX, ALL_YEARS_VALUE as FISCAL_YEAR_ALL_VALUE, } from '@/components/common/FiscalYearSelector' -import { ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Paperclip, AlertTriangle, CircleSlash, Loader2, BookOpen, X, Copy, Lock, Search, SlidersHorizontal } from 'lucide-react' +import { ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Paperclip, AlertTriangle, CircleSlash, Loader2, BookOpen, X, Copy, Lock, Search, SlidersHorizontal, RotateCcw } from 'lucide-react' import { formatDate, formatCurrency } from '@/lib/utils' import { formatVoucher } from '@/lib/bookkeeping/voucher-series-resolver' +import { resolveCurrentPeriodId } from '@/lib/bookkeeping/suggest-fiscal-period' import { Input } from '@/components/ui/input' import { AccountNumber } from '@/components/ui/account-number' import { getAccountDescription } from '@/lib/bookkeeping/account-descriptions' import JournalEntryAttachments from '@/components/bookkeeping/JournalEntryAttachments' import NoDocRequiredToggle from '@/components/bookkeeping/NoDocRequiredToggle' import CorrectionEntryDialog from '@/components/bookkeeping/CorrectionEntryDialog' +import { ConfirmationDialog } from '@/components/ui/confirmation-dialog' import JournalEntryStatusBadge from '@/components/bookkeeping/JournalEntryStatusBadge' import AttachmentPreviewSheet from '@/components/bookkeeping/AttachmentPreviewSheet' import { useToast } from '@/components/ui/use-toast' @@ -92,6 +94,8 @@ export default function JournalEntryList() { const [bulkReason, setBulkReason] = useState('') const [bulkSubmitting, setBulkSubmitting] = useState(false) const [correctionEntry, setCorrectionEntry] = useState- {t('no_results_description')} + {listMode === 'drafts' + ? t('empty_drafts_description') + : hasActiveFilters + ? t('no_results_description') + : t('empty_description')}
{t('reverse_dialog_heading', { voucher: formatVoucher(reverseEntryTarget) })}
+{t('reverse_dialog_body')}
+{t('show_swish_help')}