Add labels to automation rules (#379)

## Summary
- add label selection to automation rule create/edit dialogs
- allow rules with category, labels, or both
- display category, labels, and note actions in rule tables

## Verification
- php artisan test --compact tests/Feature/AutomationRuleTest.php
tests/Feature/AutomationRuleEvaluationTest.php
- npx eslint
resources/js/components/automation-rules/automation-rule-action-badges.tsx
resources/js/components/automation-rules/create-automation-rule-dialog.tsx
resources/js/components/automation-rules/edit-automation-rule-dialog.tsx
resources/js/components/automation-rules/automation-rules-dialog.tsx
resources/js/pages/settings/automation-rules.tsx
- npm run build (exit 0; Sentry sourcemap upload logged SSL error)
This commit is contained in:
Víctor Falcón 2026-05-11 13:56:25 +01:00 committed by GitHub
parent a20eeff378
commit 5b8e7e86d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 274 additions and 145 deletions

View File

@ -804,8 +804,10 @@
"Manage your profile and account settings": "Gestiona tu perfil y configuración de cuenta",
"Manage your subscription, update payment methods, or view invoices through the Stripe billing portal.": "Gestiona tu suscripción, actualiza métodos de pago o consulta facturas a través del portal de facturación de Stripe.",
"Manage your subscription, update payment methods, or\\n view invoices through the Stripe billing portal.": "Gestiona tu suscripción, actualiza métodos de pago o consulta facturas a través del portal de facturación de Stripe.",
"Manage rules that categorize transactions and add labels automatically": "Gestiona reglas que categorizan transacciones y agregan etiquetas automáticamente",
"Manage your transaction automation rules": "Gestiona tus reglas de automatización de transacciones",
"Manage your transaction automation rules. Rules will be applied to categorize transactions automatically.": "Gestiona tus reglas de automatización de transacciones. Las reglas se aplicarán para categorizar transacciones automáticamente.",
"Manage your transaction automation rules. Rules will categorize transactions and add labels automatically.": "Gestiona tus reglas de automatización de transacciones. Las reglas categorizarán transacciones y agregarán etiquetas automáticamente.",
"Manage your transaction categories": "Gestiona tus categorías de transacciones",
"Manage your transaction labels": "Gestiona tus etiquetas de transacciones",
"Manage your two-factor authentication settings": "Gestiona tu configuración de autenticación de dos factores",
@ -1127,6 +1129,7 @@
"Select date column": "Seleccionar columna de fecha",
"Select description column": "Seleccionar columna de descripción",
"Select invested amount column (optional)": "Seleccionar columna de cantidad invertida (opcional)",
"Select labels": "Seleccionar etiquetas",
"Select labels (optional)": "Seleccionar etiquetas (opcional)",
"Select labels...": "Seleccionar etiquetas...",
"Select language": "Seleccionar idioma",

View File

@ -0,0 +1,51 @@
import { LabelBadges } from '@/components/shared/label-combobox';
import { Badge } from '@/components/ui/badge';
import { type AutomationRule, getRuleActions } from '@/types/automation-rule';
import { getCategoryColorClasses } from '@/types/category';
import { __ } from '@/utils/i18n';
import * as Icons from 'lucide-react';
export function AutomationRuleActionBadges({ rule }: { rule: AutomationRule }) {
const actions = getRuleActions(rule);
const hasLabels = actions.hasLabels && actions.labels?.length;
if (!actions.category && !hasLabels && !actions.hasNote) {
return (
<span className="text-sm text-muted-foreground">
{__('No action set')}
</span>
);
}
return (
<div className="flex flex-wrap items-center gap-2">
{actions.category && <CategoryBadge category={actions.category} />}
{hasLabels && <LabelBadges labels={actions.labels ?? []} max={5} />}
{actions.hasNote && (
<Badge variant="outline">{__('Add note')}</Badge>
)}
</div>
);
}
function CategoryBadge({
category,
}: {
category: NonNullable<ReturnType<typeof getRuleActions>['category']>;
}) {
const colorClasses = getCategoryColorClasses(category.color);
const IconComponent = Icons[
category.icon as keyof typeof Icons
] as Icons.LucideIcon;
return (
<Badge
className={`${colorClasses.bg} ${colorClasses.text} flex items-center gap-2`}
>
{IconComponent && <IconComponent className="h-3 w-3 opacity-80" />}
{category.name}
</Badge>
);
}

View File

@ -12,14 +12,13 @@ import {
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import * as Icons from 'lucide-react';
import { MoreHorizontal } from 'lucide-react';
import { useMemo, useState } from 'react';
import { AutomationRuleActionBadges } from '@/components/automation-rules/automation-rule-action-badges';
import { CreateAutomationRuleDialog } from '@/components/automation-rules/create-automation-rule-dialog';
import { DeleteAutomationRuleDialog } from '@/components/automation-rules/delete-automation-rule-dialog';
import { EditAutomationRuleDialog } from '@/components/automation-rules/edit-automation-rule-dialog';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
ContextMenu,
@ -51,8 +50,9 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { type AutomationRule, getRuleActions } from '@/types/automation-rule';
import { type Category, getCategoryColorClasses } from '@/types/category';
import { type AutomationRule } from '@/types/automation-rule';
import { type Category } from '@/types/category';
import { type Label } from '@/types/label';
import { __ } from '@/utils/i18n';
interface AutomationRulesDialogProps {
@ -63,9 +63,11 @@ interface AutomationRulesDialogProps {
function AutomationRuleActions({
rule,
categories,
labels,
}: {
rule: AutomationRule;
categories: Category[];
labels: Label[];
}) {
const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
@ -100,6 +102,7 @@ function AutomationRuleActions({
<EditAutomationRuleDialog
rule={rule}
categories={categories}
labels={labels}
open={editOpen}
onOpenChange={setEditOpen}
/>
@ -115,9 +118,11 @@ function AutomationRuleActions({
function AutomationRuleRow({
row,
categories,
labels,
}: {
row: Row<AutomationRule>;
categories: Category[];
labels: Label[];
}) {
const rule = row.original;
const [editOpen, setEditOpen] = useState(false);
@ -163,6 +168,7 @@ function AutomationRuleRow({
<EditAutomationRuleDialog
rule={rule}
categories={categories}
labels={labels}
open={editOpen}
onOpenChange={setEditOpen}
/>
@ -183,8 +189,8 @@ export function AutomationRulesDialog({
automationRules: AutomationRule[];
}>().props;
// Get categories from globally shared Inertia data
const categories = usePage().props.categories as Category[];
const labels = usePage().props.labels as Label[];
const rules = useMemo(
() =>
rawRules.map((rule) => ({
@ -216,54 +222,7 @@ export function AutomationRulesDialog({
id: 'actions_display',
header: __('Actions'),
cell: ({ row }) => {
const rule = row.original;
const actions = getRuleActions(rule);
if (actions.type === 'multiple') {
return (
<div className="flex flex-wrap items-center gap-2">
{actions.category && (
<Badge
className={`${getCategoryColorClasses(actions.category.color).bg} ${getCategoryColorClasses(actions.category.color).text} flex items-center gap-2`}
>
{(() => {
const IconComponent = Icons[
actions.category
.icon as keyof typeof Icons
] as Icons.LucideIcon;
return IconComponent ? (
<IconComponent className="h-3 w-3 opacity-80" />
) : null;
})()}
{actions.category.name}
</Badge>
)}
</div>
);
}
if (actions.type === 'category' && actions.category) {
const colorClasses = getCategoryColorClasses(
actions.category.color,
);
const IconComponent = Icons[
actions.category.icon as keyof typeof Icons
] as Icons.LucideIcon;
return (
<Badge
className={`${colorClasses.bg} ${colorClasses.text} flex items-center gap-2`}
>
<IconComponent className="h-3 w-3 opacity-80" />
{actions.category.name}
</Badge>
);
}
return (
<span className="text-sm text-muted-foreground">
{__('No action set')}
</span>
);
return <AutomationRuleActionBadges rule={row.original} />;
},
},
{
@ -273,6 +232,7 @@ export function AutomationRulesDialog({
<AutomationRuleActions
rule={row.original}
categories={categories}
labels={labels}
/>
),
},
@ -301,7 +261,7 @@ export function AutomationRulesDialog({
<DialogTitle>{__('Automation Rules')}</DialogTitle>
<DialogDescription>
{__(
'Manage your transaction automation rules. Rules will be applied to categorize transactions automatically.',
'Manage your transaction automation rules. Rules will categorize transactions and add labels automatically.',
)}
</DialogDescription>
</DialogHeader>
@ -322,7 +282,10 @@ export function AutomationRulesDialog({
}
className="max-w-sm"
/>
<CreateAutomationRuleDialog categories={categories} />
<CreateAutomationRuleDialog
categories={categories}
labels={labels}
/>
</div>
<div className="max-h-[75vh] overflow-y-auto rounded-md border">
@ -356,6 +319,7 @@ export function AutomationRulesDialog({
key={row.id}
row={row}
categories={categories}
labels={labels}
/>
))
) : (

View File

@ -1,6 +1,7 @@
import { store } from '@/actions/App/Http/Controllers/Settings/AutomationRuleController';
import { RuleBuilder } from '@/components/automation-rules/rule-builder';
import { CategoryCombobox } from '@/components/shared/category-combobox';
import { LabelCombobox } from '@/components/shared/label-combobox';
import { Button } from '@/components/ui/button';
import { CreateButton } from '@/components/ui/create-button';
import {
@ -20,18 +21,21 @@ import {
type RuleStructure,
} from '@/lib/rule-builder-utils';
import type { Category } from '@/types/category';
import type { Label } from '@/types/label';
import { __ } from '@/utils/i18n';
import { router } from '@inertiajs/react';
import { useState } from 'react';
interface CreateAutomationRuleDialogProps {
categories: Category[];
labels: Label[];
disabled?: boolean;
onSuccess?: () => void;
}
export function CreateAutomationRuleDialog({
categories,
labels,
disabled = false,
onSuccess,
}: CreateAutomationRuleDialogProps) {
@ -42,6 +46,7 @@ export function CreateAutomationRuleDialog({
groupOperator: 'or',
});
const [categoryId, setCategoryId] = useState<string>('');
const [labelIds, setLabelIds] = useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
@ -62,10 +67,11 @@ export function CreateAutomationRuleDialog({
return;
}
if (!categoryId) {
if (!categoryId && labelIds.length === 0) {
setErrors((prev) => ({
...prev,
action_category_id: 'A category is required',
action_category_id:
'At least one category or label is required',
}));
return;
}
@ -81,10 +87,10 @@ export function CreateAutomationRuleDialog({
title: title.trim(),
priority: 0,
rules_json: JSON.stringify(jsonLogic),
action_category_id: categoryId,
action_category_id: categoryId || null,
action_note: null,
action_note_iv: null,
action_label_ids: null,
action_label_ids: labelIds,
},
{
preserveState: true,
@ -97,6 +103,7 @@ export function CreateAutomationRuleDialog({
groupOperator: 'and',
});
setCategoryId('');
setLabelIds([]);
setErrors({});
onSuccess?.();
},
@ -158,9 +165,21 @@ export function CreateAutomationRuleDialog({
<h4 className="font-medium">{__('Actions')}</h4>
<div className="space-y-2">
<FormLabel htmlFor="category">
{__('Set Category')}
</FormLabel>
<div className="flex items-center justify-between gap-2">
<FormLabel htmlFor="category">
{__('Set Category')}
</FormLabel>
{categoryId && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setCategoryId('')}
>
{__('Clear')}
</Button>
)}
</div>
<div className="mt-1">
<CategoryCombobox
value={categoryId}
@ -173,9 +192,23 @@ export function CreateAutomationRuleDialog({
</div>
</div>
{errors.action_category_id && (
<div className="space-y-2">
<FormLabel>{__('Add Labels')}</FormLabel>
<LabelCombobox
value={labelIds}
onValueChange={setLabelIds}
labels={labels}
placeholder={__('Select labels')}
/>
</div>
{(errors.action_category_id ||
errors.action_label_ids ||
errors['action_label_ids.0']) && (
<p className="text-sm text-red-500">
{errors.action_category_id}
{errors.action_category_id ||
errors.action_label_ids ||
errors['action_label_ids.0']}
</p>
)}
</div>

View File

@ -1,6 +1,7 @@
import { update } from '@/actions/App/Http/Controllers/Settings/AutomationRuleController';
import { RuleBuilder } from '@/components/automation-rules/rule-builder';
import { CategoryCombobox } from '@/components/shared/category-combobox';
import { LabelCombobox } from '@/components/shared/label-combobox';
import { Button } from '@/components/ui/button';
import {
Dialog,
@ -20,6 +21,7 @@ import {
} from '@/lib/rule-builder-utils';
import type { AutomationRule } from '@/types/automation-rule';
import type { Category } from '@/types/category';
import type { Label as LabelType } from '@/types/label';
import { __ } from '@/utils/i18n';
import { router } from '@inertiajs/react';
import { useEffect, useState } from 'react';
@ -27,6 +29,7 @@ import { useEffect, useState } from 'react';
interface EditAutomationRuleDialogProps {
rule: AutomationRule;
categories: Category[];
labels: LabelType[];
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
@ -35,6 +38,7 @@ interface EditAutomationRuleDialogProps {
export function EditAutomationRuleDialog({
rule,
categories,
labels,
open,
onOpenChange,
onSuccess,
@ -45,6 +49,7 @@ export function EditAutomationRuleDialog({
groupOperator: 'and',
});
const [categoryId, setCategoryId] = useState<string>('');
const [labelIds, setLabelIds] = useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
@ -55,6 +60,7 @@ export function EditAutomationRuleDialog({
setCategoryId(
rule.action_category_id ? String(rule.action_category_id) : '',
);
setLabelIds(rule.labels?.map((label) => label.id) ?? []);
}
}, [rule, open]);
@ -75,10 +81,11 @@ export function EditAutomationRuleDialog({
return;
}
if (!categoryId) {
if (!categoryId && labelIds.length === 0) {
setErrors((prev) => ({
...prev,
action_category_id: 'A category is required',
action_category_id:
'At least one category or label is required',
}));
return;
}
@ -94,10 +101,10 @@ export function EditAutomationRuleDialog({
title: title.trim(),
priority: rule.priority,
rules_json: JSON.stringify(jsonLogic),
action_category_id: categoryId,
action_category_id: categoryId || null,
action_note: null,
action_note_iv: null,
action_label_ids: null,
action_label_ids: labelIds,
},
{
preserveState: true,
@ -128,7 +135,7 @@ export function EditAutomationRuleDialog({
<DialogTitle>{__('Edit Automation Rule')}</DialogTitle>
<DialogDescription>
{__(
'Update the rule to automatically categorize transactions.',
'Update the rule to automatically categorize transactions and add labels.',
)}
</DialogDescription>
</DialogHeader>
@ -160,9 +167,21 @@ export function EditAutomationRuleDialog({
<h4 className="font-medium">{__('Actions')}</h4>
<div className="space-y-2">
<Label htmlFor="category">
{__('Set Category')}
</Label>
<div className="flex items-center justify-between gap-2">
<Label htmlFor="category">
{__('Set Category')}
</Label>
{categoryId && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setCategoryId('')}
>
{__('Clear')}
</Button>
)}
</div>
<CategoryCombobox
value={categoryId}
onValueChange={setCategoryId}
@ -173,9 +192,23 @@ export function EditAutomationRuleDialog({
/>
</div>
{errors.action_category_id && (
<div className="space-y-2">
<Label>{__('Add Labels')}</Label>
<LabelCombobox
value={labelIds}
onValueChange={setLabelIds}
labels={labels}
placeholder={__('Select labels')}
/>
</div>
{(errors.action_category_id ||
errors.action_label_ids ||
errors['action_label_ids.0']) && (
<p className="text-sm text-red-500">
{errors.action_category_id}
{errors.action_category_id ||
errors.action_label_ids ||
errors['action_label_ids.0']}
</p>
)}
</div>
@ -194,7 +227,9 @@ export function EditAutomationRuleDialog({
disabled={isSubmitting}
data-testid="submit-automation-rule"
>
{isSubmitting ? 'Saving...' : 'Save Changes'}
{isSubmitting
? __('Saving...')
: __('Save Changes')}
</Button>
</div>
</form>

View File

@ -59,6 +59,7 @@ import { decrypt, importKey } from '@/lib/crypto';
import { consoleDebug } from '@/lib/debug';
import { db } from '@/lib/dexie-db';
import { getStoredKey } from '@/lib/key-storage';
import { mergeReEvaluatedTransaction } from '@/lib/transaction-re-evaluation';
import { transactionSyncService } from '@/services/transaction-sync';
import { type Account, type Bank } from '@/types/account';
import { type AutomationRule } from '@/types/automation-rule';
@ -861,14 +862,9 @@ export function TransactionList({
const updated = response.data.data;
updateTransaction({
...transaction,
category_id: updated.category_id,
category: updated.category,
labels: updated.labels,
notes: updated.notes,
notes_iv: updated.notes_iv,
});
updateTransaction(
mergeReEvaluatedTransaction(transaction, updated),
);
consoleDebug('✓ UI state updated successfully');
} catch (error) {
consoleDebug('❌ Error during re-evaluation:', error);

View File

@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import { type DecryptedTransaction } from '@/types/transaction';
import { mergeReEvaluatedTransaction } from './transaction-re-evaluation';
function transaction(
overrides: Partial<DecryptedTransaction> = {},
): DecryptedTransaction {
return {
id: 'transaction-1',
user_id: 'user-1',
account_id: 'account-1',
category_id: null,
description: 'Coffee',
description_iv: null,
transaction_date: '2026-05-11',
amount: -450,
currency_code: 'EUR',
notes: null,
notes_iv: null,
source: 'imported',
label_ids: [],
created_at: '2026-05-11T00:00:00.000000Z',
updated_at: '2026-05-11T00:00:00.000000Z',
decryptedDescription: 'Coffee',
decryptedNotes: null,
...overrides,
};
}
describe('mergeReEvaluatedTransaction', () => {
it('derives label ids from returned labels', () => {
const merged = mergeReEvaluatedTransaction(
transaction(),
transaction({
labels: [
{
id: 'label-1',
user_id: 'user-1',
name: 'Work',
color: 'blue',
created_at: '2026-05-11T00:00:00.000000Z',
updated_at: '2026-05-11T00:00:00.000000Z',
deleted_at: null,
},
],
}),
);
expect(merged.label_ids).toEqual(['label-1']);
});
it('updates plaintext notes for immediate display', () => {
const merged = mergeReEvaluatedTransaction(
transaction(),
transaction({
notes: 'Needs review',
notes_iv: null,
}),
);
expect(merged.decryptedNotes).toBe('Needs review');
});
});

View File

@ -0,0 +1,25 @@
import { type DecryptedTransaction } from '@/types/transaction';
export function mergeReEvaluatedTransaction(
transaction: DecryptedTransaction,
updated: DecryptedTransaction,
): DecryptedTransaction {
const labels = updated.labels ?? transaction.labels;
const labelIds = updated.labels
? updated.labels.map((label) => label.id)
: (updated.label_ids ?? transaction.label_ids);
return {
...transaction,
category_id: updated.category_id,
category: updated.category,
labels,
label_ids: labelIds,
notes: updated.notes,
notes_iv: updated.notes_iv,
decryptedNotes:
updated.notes_iv === null
? (updated.notes ?? null)
: transaction.decryptedNotes,
};
}

View File

@ -12,16 +12,15 @@ import {
useReactTable,
VisibilityState,
} from '@tanstack/react-table';
import * as Icons from 'lucide-react';
import { MoreHorizontal } from 'lucide-react';
import { useMemo, useState } from 'react';
import { index as automationRulesIndex } from '@/actions/App/Http/Controllers/Settings/AutomationRuleController';
import { AutomationRuleActionBadges } from '@/components/automation-rules/automation-rule-action-badges';
import { CreateAutomationRuleDialog } from '@/components/automation-rules/create-automation-rule-dialog';
import { DeleteAutomationRuleDialog } from '@/components/automation-rules/delete-automation-rule-dialog';
import { EditAutomationRuleDialog } from '@/components/automation-rules/edit-automation-rule-dialog';
import HeadingSmall from '@/components/heading-small';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
ContextMenu,
@ -49,8 +48,9 @@ import {
import AppLayout from '@/layouts/app-layout';
import SettingsLayout from '@/layouts/settings/layout';
import { type BreadcrumbItem } from '@/types';
import { type AutomationRule, getRuleActions } from '@/types/automation-rule';
import { type Category, getCategoryColorClasses } from '@/types/category';
import { type AutomationRule } from '@/types/automation-rule';
import { type Category } from '@/types/category';
import { type Label } from '@/types/label';
import { __ } from '@/utils/i18n';
const breadcrumbs: BreadcrumbItem[] = [
@ -63,9 +63,11 @@ const breadcrumbs: BreadcrumbItem[] = [
function AutomationRuleActions({
rule,
categories,
labels,
}: {
rule: AutomationRule;
categories: Category[];
labels: Label[];
}) {
const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
@ -100,6 +102,7 @@ function AutomationRuleActions({
<EditAutomationRuleDialog
rule={rule}
categories={categories}
labels={labels}
open={editOpen}
onOpenChange={setEditOpen}
/>
@ -115,9 +118,11 @@ function AutomationRuleActions({
function AutomationRuleRow({
row,
categories,
labels,
}: {
row: Row<AutomationRule>;
categories: Category[];
labels: Label[];
}) {
const rule = row.original;
const [editOpen, setEditOpen] = useState(false);
@ -163,6 +168,7 @@ function AutomationRuleRow({
<EditAutomationRuleDialog
rule={rule}
categories={categories}
labels={labels}
open={editOpen}
onOpenChange={setEditOpen}
/>
@ -180,8 +186,8 @@ export default function AutomationRules() {
automationRules: AutomationRule[];
}>().props;
// Get categories from globally shared Inertia data
const categories = usePage().props.categories as Category[];
const labels = usePage().props.labels as Label[];
const rules = useMemo(
() =>
rawRules.map((rule) => ({
@ -213,54 +219,7 @@ export default function AutomationRules() {
id: 'actions_display',
header: __('Actions'),
cell: ({ row }) => {
const rule = row.original;
const actions = getRuleActions(rule);
if (actions.type === 'multiple') {
return (
<div className="flex flex-wrap items-center gap-2">
{actions.category && (
<Badge
className={`${getCategoryColorClasses(actions.category.color).bg} ${getCategoryColorClasses(actions.category.color).text} flex items-center gap-2`}
>
{(() => {
const IconComponent = Icons[
actions.category
.icon as keyof typeof Icons
] as Icons.LucideIcon;
return IconComponent ? (
<IconComponent className="h-3 w-3 opacity-80" />
) : null;
})()}
{actions.category.name}
</Badge>
)}
</div>
);
}
if (actions.type === 'category' && actions.category) {
const colorClasses = getCategoryColorClasses(
actions.category.color,
);
const IconComponent = Icons[
actions.category.icon as keyof typeof Icons
] as Icons.LucideIcon;
return (
<Badge
className={`${colorClasses.bg} ${colorClasses.text} flex items-center gap-2`}
>
<IconComponent className="h-3 w-3 opacity-80" />
{actions.category.name}
</Badge>
);
}
return (
<span className="text-sm text-muted-foreground">
{__('No action set')}
</span>
);
return <AutomationRuleActionBadges rule={row.original} />;
},
},
{
@ -270,6 +229,7 @@ export default function AutomationRules() {
<AutomationRuleActions
rule={row.original}
categories={categories}
labels={labels}
/>
),
},
@ -300,7 +260,7 @@ export default function AutomationRules() {
<HeadingSmall
title={__('Automation rules settings')}
description={__(
'Manage your transaction automation rules',
'Manage rules that categorize transactions and add labels automatically',
)}
/>
@ -322,6 +282,7 @@ export default function AutomationRules() {
/>
<CreateAutomationRuleDialog
categories={categories}
labels={labels}
/>
</div>
@ -363,6 +324,7 @@ export default function AutomationRules() {
key={row.id}
row={row}
categories={categories}
labels={labels}
/>
))
) : (

View File

@ -67,6 +67,7 @@ import {
isCursorPaginatedResponse,
} from '@/lib/cursor-pagination';
import { consoleDebug } from '@/lib/debug';
import { mergeReEvaluatedTransaction } from '@/lib/transaction-re-evaluation';
import { cn } from '@/lib/utils';
import { transactionSyncService } from '@/services/transaction-sync';
import { type BreadcrumbItem } from '@/types';
@ -662,14 +663,9 @@ export default function Transactions({
const updated = response.data.data;
updateTransaction({
...transaction,
category_id: updated.category_id,
category: updated.category,
labels: updated.labels,
notes: updated.notes,
notes_iv: updated.notes_iv,
});
updateTransaction(
mergeReEvaluatedTransaction(transaction, updated),
);
} catch (error) {
console.error('Failed to re-evaluate rules:', error);
} finally {