mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 19:16:40 +07:00
feat: split a transaction across multiple categories
Один чек часто смешивает несколько категорий трат (гипермаркет: часть — продукты, часть — бытовая химия). Раньше такую транзакцию приходилось огрублять до одной категории или дробить вручную на несколько записей. Теперь у транзакции есть второй режим: 2+ доли (TransactionSplit) с суммами по разным категориям, которые обязаны в точности складываться в сумму транзакции. - Новая модель TransactionSplit + миграция. - split_rule_violation() проверяет инварианты на создании и редактировании. - category_rollup.py — общий helper, которым теперь пользуются Dashboard, отчёт по категориям и бюджеты, учитывая доли сплитов наравне с обычными транзакциями. - Фильтр по категории в списке транзакций находит и сплит-транзакции. - Сплиты попадают в полный бэкап/восстановление БД. - 13 новых backend-тестов. - Фронтенд: переключатель «Разбить на несколько категорий» в форме транзакции с повторяемыми строками и индикатором остатка суммы; строка транзакции в списке показывает все категории сплита.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { ArrowLeftRight, CalendarSearch, Pencil, StickyNote, Trash2 } from "lucide-react";
|
||||
import { ArrowLeftRight, CalendarSearch, Pencil, SquareDivide, StickyNote, Trash2 } from "lucide-react";
|
||||
import { Dialog } from "@/components/ui/Dialog";
|
||||
import { getCategoryIcon } from "@/lib/icons";
|
||||
import { formatCurrency, formatTransactionDate } from "@/lib/format";
|
||||
@@ -35,8 +35,14 @@ export function TransactionsTable({ items, onEdit, onDelete, onJumpToMonth }: Tr
|
||||
{items.map((tx) => {
|
||||
const isTransfer = tx.type === "transfer";
|
||||
const isExpense = tx.type === "expense";
|
||||
const Icon = isTransfer ? ArrowLeftRight : getCategoryIcon(tx.category?.icon);
|
||||
const color = isTransfer ? "var(--text-muted)" : tx.category?.color ?? "var(--text-muted)";
|
||||
const isSplit = tx.splits.length > 0;
|
||||
const Icon = isTransfer ? ArrowLeftRight : isSplit ? SquareDivide : getCategoryIcon(tx.category?.icon);
|
||||
const color = isTransfer || isSplit ? "var(--text-muted)" : tx.category?.color ?? "var(--text-muted)";
|
||||
const categoryLabel = isSplit
|
||||
? tx.splits.map((split) => (split.category ? translateCategoryName(split.category.name) : "?")).join(" + ")
|
||||
: tx.category
|
||||
? translateCategoryName(tx.category.name)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<li key={tx.id} className="group flex items-center gap-3 py-3">
|
||||
@@ -52,7 +58,7 @@ export function TransactionsTable({ items, onEdit, onDelete, onJumpToMonth }: Tr
|
||||
<span className="block truncate text-xs text-text-muted">
|
||||
{formatTransactionDate(tx.date, Boolean(onJumpToMonth))} · {tx.account.name}
|
||||
{isTransfer && tx.transfer_account_id ? ` ${t("transactions.transferSuffix")}` : ""}
|
||||
{tx.category ? ` · ${translateCategoryName(tx.category.name)}` : ""}
|
||||
{categoryLabel ? ` · ${categoryLabel}` : ""}
|
||||
{tx.tags.length > 0 ? ` · ${tx.tags.map((tag) => tag.name).join(", ")}` : ""}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user