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:
co-authored by
Claude Sonnet 4.6
parent
5279dfb494
commit
a4da2d62df
@@ -21,7 +21,7 @@ import {
|
|||||||
DropdownMenuRadioGroup,
|
DropdownMenuRadioGroup,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuRadioItem,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} 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 TransactionForm from '@/components/transactions/TransactionForm'
|
||||||
import BatchCategorySelector from '@/components/transactions/BatchCategorySelector'
|
import BatchCategorySelector from '@/components/transactions/BatchCategorySelector'
|
||||||
import TransactionStatusBar from '@/components/transactions/TransactionStatusBar'
|
import TransactionStatusBar from '@/components/transactions/TransactionStatusBar'
|
||||||
@@ -1667,6 +1667,67 @@ export default function TransactionsPage() {
|
|||||||
exitBatchMode()
|
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) {
|
async function handleBatchCategorize(category: TransactionCategory, vatTreatment?: VatTreatment) {
|
||||||
const ids = Array.from(selectedIds)
|
const ids = Array.from(selectedIds)
|
||||||
setBatchProgress({ done: 0, total: ids.length })
|
setBatchProgress({ done: 0, total: ids.length })
|
||||||
@@ -2011,6 +2072,7 @@ export default function TransactionsPage() {
|
|||||||
onOpenAttachDocument={openAttachDocumentDialog}
|
onOpenAttachDocument={openAttachDocumentDialog}
|
||||||
onOpenCategoryDialog={openCategoryDialog}
|
onOpenCategoryDialog={openCategoryDialog}
|
||||||
onDelete={handleDeleteTransaction}
|
onDelete={handleDeleteTransaction}
|
||||||
|
onIgnore={handleIgnoreTransaction}
|
||||||
onEditTitle={openEditTitleDialog}
|
onEditTitle={openEditTitleDialog}
|
||||||
onToggleSelect={toggleBatchSelect}
|
onToggleSelect={toggleBatchSelect}
|
||||||
/>
|
/>
|
||||||
@@ -2065,6 +2127,14 @@ export default function TransactionsPage() {
|
|||||||
<X className="mr-1 h-3 w-3" />
|
<X className="mr-1 h-3 w-3" />
|
||||||
Avmarkera
|
Avmarkera
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBatchIgnore}
|
||||||
|
>
|
||||||
|
<EyeOff className="mr-1 h-3 w-3" />
|
||||||
|
Ignorera
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
ArrowDownRight,
|
ArrowDownRight,
|
||||||
|
EyeOff,
|
||||||
FileSearch,
|
FileSearch,
|
||||||
FileText,
|
FileText,
|
||||||
Link2,
|
Link2,
|
||||||
@@ -73,6 +74,8 @@ interface TransactionInboxCardProps {
|
|||||||
onOpenAttachDocument?: (transaction: TransactionWithInvoice) => void
|
onOpenAttachDocument?: (transaction: TransactionWithInvoice) => void
|
||||||
onOpenCategoryDialog: (transaction: TransactionWithInvoice) => void
|
onOpenCategoryDialog: (transaction: TransactionWithInvoice) => void
|
||||||
onDelete?: (id: string) => 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. */
|
/** Open the edit-title dialog. Only wired for editable (unbooked/unmatched) rows. */
|
||||||
onEditTitle?: (transaction: TransactionWithInvoice) => void
|
onEditTitle?: (transaction: TransactionWithInvoice) => void
|
||||||
onToggleSelect: (id: string) => void
|
onToggleSelect: (id: string) => void
|
||||||
@@ -92,6 +95,7 @@ export default function TransactionInboxCard({
|
|||||||
onOpenAttachDocument,
|
onOpenAttachDocument,
|
||||||
onOpenCategoryDialog,
|
onOpenCategoryDialog,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onIgnore,
|
||||||
onEditTitle,
|
onEditTitle,
|
||||||
onToggleSelect,
|
onToggleSelect,
|
||||||
onAnimationComplete,
|
onAnimationComplete,
|
||||||
@@ -238,9 +242,10 @@ export default function TransactionInboxCard({
|
|||||||
const showAttachDocumentItem = isUnbooked && canWrite && !!onOpenAttachDocument
|
const showAttachDocumentItem = isUnbooked && canWrite && !!onOpenAttachDocument
|
||||||
const showSplitItem = showInvoiceMatchButton && !!onOpenSplitMatch
|
const showSplitItem = showInvoiceMatchButton && !!onOpenSplitMatch
|
||||||
const showEditItem = isTitleEditable && !!onEditTitle
|
const showEditItem = isTitleEditable && !!onEditTitle
|
||||||
|
const showIgnoreItem = isUnbooked && isImportedTransaction(transaction) && !!onIgnore
|
||||||
const showDeleteItem = canDelete && !!onDelete
|
const showDeleteItem = canDelete && !!onDelete
|
||||||
const showOverflowMenu =
|
const showOverflowMenu =
|
||||||
showMatchVoucherItem || showAttachDocumentItem || showSplitItem || showEditItem || showDeleteItem
|
showMatchVoucherItem || showAttachDocumentItem || showSplitItem || showEditItem || showIgnoreItem || showDeleteItem
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -390,20 +395,31 @@ export default function TransactionInboxCard({
|
|||||||
{t('edit_title_aria')}
|
{t('edit_title_aria')}
|
||||||
</DropdownMenuItem>
|
</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 && (
|
{showDeleteItem && (
|
||||||
<>
|
<DropdownMenuItem
|
||||||
{(showMatchVoucherItem || showAttachDocumentItem || showSplitItem || showEditItem) && <DropdownMenuSeparator />}
|
className="text-destructive focus:text-destructive"
|
||||||
<DropdownMenuItem
|
onClick={(e) => {
|
||||||
className="text-destructive focus:text-destructive"
|
e.stopPropagation()
|
||||||
onClick={(e) => {
|
onDelete!(transaction.id)
|
||||||
e.stopPropagation()
|
}}
|
||||||
onDelete!(transaction.id)
|
>
|
||||||
}}
|
<Trash2 className="h-4 w-4" />
|
||||||
>
|
{t('delete_aria')}
|
||||||
<Trash2 className="h-4 w-4" />
|
</DropdownMenuItem>
|
||||||
{t('delete_aria')}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -1720,6 +1720,7 @@
|
|||||||
"match_voucher_btn": "Match to existing voucher",
|
"match_voucher_btn": "Match to existing voucher",
|
||||||
"attach_document_btn": "Match to document",
|
"attach_document_btn": "Match to document",
|
||||||
"more_actions_aria": "More actions",
|
"more_actions_aria": "More actions",
|
||||||
|
"ignore_btn": "Ignore transaction",
|
||||||
"delete_aria": "Delete transaction",
|
"delete_aria": "Delete transaction",
|
||||||
"edit_title_aria": "Edit title",
|
"edit_title_aria": "Edit title",
|
||||||
"edited_badge": "edited",
|
"edited_badge": "edited",
|
||||||
|
|||||||
@@ -1720,6 +1720,7 @@
|
|||||||
"match_voucher_btn": "Matcha mot befintlig verifikation",
|
"match_voucher_btn": "Matcha mot befintlig verifikation",
|
||||||
"attach_document_btn": "Matcha mot underlag",
|
"attach_document_btn": "Matcha mot underlag",
|
||||||
"more_actions_aria": "Fler åtgärder",
|
"more_actions_aria": "Fler åtgärder",
|
||||||
|
"ignore_btn": "Ignorera transaktion",
|
||||||
"delete_aria": "Ta bort transaktion",
|
"delete_aria": "Ta bort transaktion",
|
||||||
"edit_title_aria": "Ändra titel",
|
"edit_title_aria": "Ändra titel",
|
||||||
"edited_badge": "redigerad",
|
"edited_badge": "redigerad",
|
||||||
|
|||||||
Reference in New Issue
Block a user