fix: remove delete button from list view — use detail page instead (#232)
The client-side lastInSeriesIds computation only sees the current page of entries (20 at a time), producing false positives when the true last voucher is on a different page or in a different fiscal period. Delete is now only available from the detail page (/bookkeeping/[id]) where is_last_in_series is computed server-side from the full dataset. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4644642f8a
commit
2fcebffb34
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -13,8 +13,6 @@ import { AccountNumber } from '@/components/ui/account-number'
|
||||
import { getAccountDescription } from '@/lib/bookkeeping/account-descriptions'
|
||||
import JournalEntryAttachments from '@/components/bookkeeping/JournalEntryAttachments'
|
||||
import CorrectionEntryDialog from '@/components/bookkeeping/CorrectionEntryDialog'
|
||||
import { ConfirmationDialog } from '@/components/ui/confirmation-dialog'
|
||||
import { useToast } from '@/components/ui/use-toast'
|
||||
import JournalEntryStatusBadge from '@/components/bookkeeping/JournalEntryStatusBadge'
|
||||
import type { JournalEntry, JournalEntryLine } from '@/types'
|
||||
|
||||
@@ -32,7 +30,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function JournalEntryList({ periodId }: Props) {
|
||||
const { toast } = useToast()
|
||||
const [entries, setEntries] = useState<JournalEntry[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
@@ -41,8 +38,6 @@ export default function JournalEntryList({ periodId }: Props) {
|
||||
const [attachmentCounts, setAttachmentCounts] = useState<Record<string, number>>({})
|
||||
const [showMissingOnly, setShowMissingOnly] = useState(false)
|
||||
const [correctionEntry, setCorrectionEntry] = useState<JournalEntry | null>(null)
|
||||
const [deleteEntry, setDeleteEntry] = useState<JournalEntry | null>(null)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [dateSortDir, setDateSortDir] = useState<'desc' | 'asc'>('desc')
|
||||
const [dateFrom, setDateFrom] = useState('')
|
||||
const [dateTo, setDateTo] = useState('')
|
||||
@@ -139,43 +134,6 @@ export default function JournalEntryList({ periodId }: Props) {
|
||||
setAttachmentCounts((prev) => ({ ...prev, [entryId]: count }))
|
||||
}, [])
|
||||
|
||||
// Compute which entries are the last in their series (enables delete button)
|
||||
const lastInSeriesIds = useMemo(() => {
|
||||
const maxPerSeries = new Map<string, { id: string; num: number }>()
|
||||
for (const e of entries) {
|
||||
if (e.status !== 'posted') continue
|
||||
const key = `${e.fiscal_period_id}:${e.voucher_series}`
|
||||
const current = maxPerSeries.get(key)
|
||||
if (!current || e.voucher_number > current.num) {
|
||||
maxPerSeries.set(key, { id: e.id, num: e.voucher_number })
|
||||
}
|
||||
}
|
||||
return new Set(Array.from(maxPerSeries.values()).map(v => v.id))
|
||||
}, [entries])
|
||||
|
||||
const handleDeleteEntry = useCallback(async () => {
|
||||
if (!deleteEntry) return
|
||||
setIsDeleting(true)
|
||||
try {
|
||||
const res = await fetch(`/api/bookkeeping/journal-entries/${deleteEntry.id}`, { method: 'DELETE' })
|
||||
const result = await res.json()
|
||||
if (res.ok) {
|
||||
toast({
|
||||
title: 'Verifikat raderat',
|
||||
description: `Verifikat ${result.data?.voucher_series ?? ''}${result.data?.voucher_number ?? ''} har raderats.`,
|
||||
})
|
||||
setDeleteEntry(null)
|
||||
fetchEntries()
|
||||
} else {
|
||||
toast({ title: 'Kunde inte radera', description: result.error, variant: 'destructive' })
|
||||
}
|
||||
} catch {
|
||||
toast({ title: 'Kunde inte radera verifikat', variant: 'destructive' })
|
||||
} finally {
|
||||
setIsDeleting(false)
|
||||
}
|
||||
}, [deleteEntry, toast])
|
||||
|
||||
const toggleExpand = (id: string) => {
|
||||
setExpandedId(expandedId === id ? null : id)
|
||||
}
|
||||
@@ -477,16 +435,6 @@ export default function JournalEntryList({ periodId }: Props) {
|
||||
<Button variant="outline" size="sm" className="w-full sm:w-auto" asChild>
|
||||
<Link href={`/bookkeeping/${entry.id}`}>Visa detaljer</Link>
|
||||
</Button>
|
||||
{entry.status === 'posted' && lastInSeriesIds.has(entry.id) && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => setDeleteEntry(entry)}
|
||||
>
|
||||
Radera
|
||||
</Button>
|
||||
)}
|
||||
{entry.status === 'posted' && entry.source_type !== 'storno' && entry.source_type !== 'correction' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -515,28 +463,6 @@ export default function JournalEntryList({ periodId }: Props) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Delete confirmation dialog */}
|
||||
<ConfirmationDialog
|
||||
open={!!deleteEntry}
|
||||
onOpenChange={(open) => { if (!open) setDeleteEntry(null) }}
|
||||
onConfirm={handleDeleteEntry}
|
||||
isSubmitting={isDeleting}
|
||||
title="Radera verifikat"
|
||||
warningText={deleteEntry ? `Verifikat ${deleteEntry.voucher_series}${deleteEntry.voucher_number} raderas permanent. Denna åtgärd kan inte ångras.` : ''}
|
||||
confirmLabel="Radera permanent"
|
||||
>
|
||||
<div className="flex items-start gap-3 rounded-lg border border-destructive/30 bg-destructive/10 p-4">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive mt-0.5 shrink-0" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium mb-1">Permanent radering</p>
|
||||
<p className="text-muted-foreground">
|
||||
Verifikatet och dess kontorader tas bort. Kopplade transaktioner och
|
||||
fakturor markeras som ej bokförda. Underlag behålls men avlänkas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmationDialog>
|
||||
|
||||
{/* Pagination */}
|
||||
{count > pageSize && (
|
||||
<div className="flex justify-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user