feat(splits): save split as rule from split modal

- Checkbox in split modal: 'Also save as rule for <merchant>'
- Creates a rule with apply_split action storing the participant shares
- Rules engine now handles apply_split: deletes existing splits and re-applies
- Bulk split mode hides the checkbox (rule wouldn't make sense for ad-hoc bulk)
This commit is contained in:
2026-03-11 12:40:03 +11:00
parent a8743ba7df
commit af4c64bba7
4 changed files with 58 additions and 6 deletions
+18
View File
@@ -9,10 +9,16 @@ interface Condition {
value: string;
}
interface SplitEntry {
participant_id: number;
share_percent: number;
}
interface Actions {
set_category?: string;
add_tag_ids?: number[];
set_merchant?: string;
apply_split?: SplitEntry[];
}
interface TxFields {
@@ -108,6 +114,18 @@ export async function POST(req: NextRequest) {
);
}
}
if (actions.apply_split?.length) {
// Delete existing splits then insert new ones
await queryRaw(`DELETE FROM transaction_splits WHERE transaction_id = $1`, [tx.id]);
for (const s of actions.apply_split) {
await queryRaw(
`INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
[tx.id, s.participant_id, s.share_percent]
);
}
}
}
}
+3 -2
View File
@@ -232,7 +232,7 @@ function TransactionsContent() {
const [selected, setSelected] = useState<Set<number>>(new Set());
const [bulkCategory, setBulkCategory] = useState("");
const [bulkTagId, setBulkTagId] = useState("");
const [splitModal, setSplitModal] = useState<{ transactionId?: number; transactionIds?: number[]; amount?: number; description: string } | null>(null);
const [splitModal, setSplitModal] = useState<{ transactionId?: number; transactionIds?: number[]; amount?: number; description: string; merchant?: string } | null>(null);
const [rulePrompt, setRulePrompt] = useState<{
tx: { id: number; effective_merchant: string; description: string; bank_name: string };
field: "category" | "merchant";
@@ -543,7 +543,7 @@ function TransactionsContent() {
</td>
<td className="p-2">
<button
onClick={() => setSplitModal({ transactionId: t.id, amount: t.amount, description: t.description, transactionIds: undefined })}
onClick={() => setSplitModal({ transactionId: t.id, amount: t.amount, description: t.description, merchant: t.effective_merchant || undefined, transactionIds: undefined })}
className="text-xs text-zinc-500 hover:text-zinc-200 px-2 py-0.5 rounded hover:bg-zinc-800 transition-colors"
title="Split this transaction"
>
@@ -564,6 +564,7 @@ function TransactionsContent() {
transactionIds={splitModal.transactionIds}
amount={splitModal.amount}
description={splitModal.description}
merchant={splitModal.merchant}
onClose={() => { setSplitModal(null); if (splitModal.transactionIds) setSelected(new Set()); }}
/>
)}