feat: add ignore menu item to transaction inbox card (#758)

Imported bank transactions can now be hidden from the inbox via a new
"Ignorera transaktion" item in the ⋯ overflow menu. The backend
(POST/DELETE /api/transactions/[id]/ignore) and the is_ignored DB column
already existed; this wires up the missing UI affordance.

- TransactionInboxCard: add onIgnore prop + EyeOff menu item (imported
  rows only — manually created rows use delete instead)
- page.tsx: pass onIgnore={handleIgnoreTransaction} to the card; add
  handleBatchIgnore for the batch action bar's new "Ignorera" button
- sv.json / en.json: add ignore_btn translation key to tx_inbox_card

Signed-off-by: Jonas Flodén <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jonas Flodén
2026-06-25 12:40:05 +02:00
committed by GitHub
parent 5279dfb494
commit a4da2d62df
4 changed files with 103 additions and 15 deletions
+71 -1
View File
@@ -21,7 +21,7 @@ import {
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
} from '@/components/ui/dropdown-menu'
import { ChevronDown, Layers, Search, Trash2, X } from 'lucide-react'
import { ChevronDown, EyeOff, Layers, Search, Trash2, X } from 'lucide-react'
import TransactionForm from '@/components/transactions/TransactionForm'
import BatchCategorySelector from '@/components/transactions/BatchCategorySelector'
import TransactionStatusBar from '@/components/transactions/TransactionStatusBar'
@@ -1667,6 +1667,67 @@ export default function TransactionsPage() {
exitBatchMode()
}
async function handleBatchIgnore() {
const ids = Array.from(selectedIds)
const ok = await confirm({
title: `Ignorera ${ids.length} transaktioner?`,
description: 'Transaktionerna försvinner från listan utan att bokföras. Du kan återställa dem under Bankavstämning.',
confirmLabel: 'Ignorera',
cancelLabel: 'Avbryt',
variant: 'warning',
})
if (!ok) return
const ignoredIds = new Set<string>()
setBatchProgress({ done: 0, total: ids.length })
let successes = 0
const failures: string[] = []
for (let i = 0; i < ids.length; i++) {
try {
const res = await fetch(`/api/transactions/${ids[i]}/ignore`, { method: 'POST' })
if (res.ok) {
successes++
ignoredIds.add(ids[i])
} else {
const tx = transactions.find((t) => t.id === ids[i])
failures.push(tx?.description || ids[i])
}
} catch {
failures.push(ids[i])
}
setBatchProgress({ done: i + 1, total: ids.length })
}
if (ignoredIds.size > 0) {
setExitingIds((prev) => {
const next = new Set(prev)
for (const id of ignoredIds) next.add(id)
return next
})
setTotalUncategorizedCount((prev) => Math.max(0, (prev ?? ignoredIds.size) - ignoredIds.size))
setTimeout(() => {
setTransactions((prev) =>
prev.map((t) => (ignoredIds.has(t.id) ? { ...t, is_ignored: true } : t))
)
setExitingIds((prev) => {
const next = new Set(prev)
for (const id of ignoredIds) next.delete(id)
return next
})
}, 350)
}
setBatchProgress(null)
if (failures.length === 0) {
toast({ title: 'Klart', description: `${successes} transaktioner ignorerade` })
} else {
toast({
title: 'Delvis klart',
description: `${successes} ignorerade, ${failures.length} misslyckades`,
variant: 'destructive',
})
}
exitBatchMode()
}
async function handleBatchCategorize(category: TransactionCategory, vatTreatment?: VatTreatment) {
const ids = Array.from(selectedIds)
setBatchProgress({ done: 0, total: ids.length })
@@ -2011,6 +2072,7 @@ export default function TransactionsPage() {
onOpenAttachDocument={openAttachDocumentDialog}
onOpenCategoryDialog={openCategoryDialog}
onDelete={handleDeleteTransaction}
onIgnore={handleIgnoreTransaction}
onEditTitle={openEditTitleDialog}
onToggleSelect={toggleBatchSelect}
/>
@@ -2065,6 +2127,14 @@ export default function TransactionsPage() {
<X className="mr-1 h-3 w-3" />
Avmarkera
</Button>
<Button
variant="outline"
size="sm"
onClick={handleBatchIgnore}
>
<EyeOff className="mr-1 h-3 w-3" />
Ignorera
</Button>
<Button
variant="outline"
size="sm"
@@ -20,6 +20,7 @@ import {
AlertCircle,
ArrowUpRight,
ArrowDownRight,
EyeOff,
FileSearch,
FileText,
Link2,
@@ -73,6 +74,8 @@ interface TransactionInboxCardProps {
onOpenAttachDocument?: (transaction: TransactionWithInvoice) => void
onOpenCategoryDialog: (transaction: TransactionWithInvoice) => void
onDelete?: (id: string) => void
/** Mark the transaction as ignored so it leaves the inbox without a journal entry. */
onIgnore?: (transaction: TransactionWithInvoice) => void
/** Open the edit-title dialog. Only wired for editable (unbooked/unmatched) rows. */
onEditTitle?: (transaction: TransactionWithInvoice) => void
onToggleSelect: (id: string) => void
@@ -92,6 +95,7 @@ export default function TransactionInboxCard({
onOpenAttachDocument,
onOpenCategoryDialog,
onDelete,
onIgnore,
onEditTitle,
onToggleSelect,
onAnimationComplete,
@@ -238,9 +242,10 @@ export default function TransactionInboxCard({
const showAttachDocumentItem = isUnbooked && canWrite && !!onOpenAttachDocument
const showSplitItem = showInvoiceMatchButton && !!onOpenSplitMatch
const showEditItem = isTitleEditable && !!onEditTitle
const showIgnoreItem = isUnbooked && isImportedTransaction(transaction) && !!onIgnore
const showDeleteItem = canDelete && !!onDelete
const showOverflowMenu =
showMatchVoucherItem || showAttachDocumentItem || showSplitItem || showEditItem || showDeleteItem
showMatchVoucherItem || showAttachDocumentItem || showSplitItem || showEditItem || showIgnoreItem || showDeleteItem
return (
<motion.div
@@ -390,20 +395,31 @@ export default function TransactionInboxCard({
{t('edit_title_aria')}
</DropdownMenuItem>
)}
{(showIgnoreItem || showDeleteItem) && (showMatchVoucherItem || showAttachDocumentItem || showSplitItem || showEditItem) && (
<DropdownMenuSeparator />
)}
{showIgnoreItem && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
onIgnore!(transaction)
}}
>
<EyeOff className="h-4 w-4" />
{t('ignore_btn')}
</DropdownMenuItem>
)}
{showDeleteItem && (
<>
{(showMatchVoucherItem || showAttachDocumentItem || showSplitItem || showEditItem) && <DropdownMenuSeparator />}
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
e.stopPropagation()
onDelete!(transaction.id)
}}
>
<Trash2 className="h-4 w-4" />
{t('delete_aria')}
</DropdownMenuItem>
</>
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
e.stopPropagation()
onDelete!(transaction.id)
}}
>
<Trash2 className="h-4 w-4" />
{t('delete_aria')}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
+1
View File
@@ -1720,6 +1720,7 @@
"match_voucher_btn": "Match to existing voucher",
"attach_document_btn": "Match to document",
"more_actions_aria": "More actions",
"ignore_btn": "Ignore transaction",
"delete_aria": "Delete transaction",
"edit_title_aria": "Edit title",
"edited_badge": "edited",
+1
View File
@@ -1720,6 +1720,7 @@
"match_voucher_btn": "Matcha mot befintlig verifikation",
"attach_document_btn": "Matcha mot underlag",
"more_actions_aria": "Fler åtgärder",
"ignore_btn": "Ignorera transaktion",
"delete_aria": "Ta bort transaktion",
"edit_title_aria": "Ändra titel",
"edited_badge": "redigerad",