From d1458eb1600a6b95868e091b9c26aa048b5633f1 Mon Sep 17 00:00:00 2001 From: ZProger Date: Sun, 16 Aug 2026 23:50:10 +0300 Subject: [PATCH] feat: view and edit a transaction's note from the list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transactions can carry a `notes` field (set via the API — e.g. the currency-conversion metadata on this user's imported historical data), but there was no way to see it in the UI at all. TransactionsTable now shows a note icon next to edit/delete, only when a transaction actually has one, opening a dialog with the text. The edit form's `notes` field was already wired end to end (state, payload) but had no input in the JSX — added it, since a view-only note with no way to edit or clear it would be a dead end. Also fixed e2e/run.sh's readiness check, found while running the full suite for this change: it polled `/` (nginx, answers almost immediately) instead of `/api/health`, so the first test could start before the backend finished migrating/seeding and fail trying to parse nginx's HTML error page as JSON. --- backend/app/core/config.py | 2 +- e2e/run.sh | 8 +- frontend/package.json | 2 +- .../transactions/TransactionFormModal.tsx | 9 ++ .../transactions/TransactionsTable.tsx | 127 +++++++++++------- frontend/src/lib/i18n.ts | 6 + 6 files changed, 98 insertions(+), 56 deletions(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 0272bfa..274368b 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -6,7 +6,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict # Single source of truth for the running app's version — surfaced in the API # title and embedded in exported backups so an old file can be told apart # from a current one. -APP_VERSION = "0.19.2" +APP_VERSION = "0.20.0" class Settings(BaseSettings): diff --git a/e2e/run.sh b/e2e/run.sh index 37d7c0d..00f6696 100755 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -23,9 +23,13 @@ trap cleanup EXIT $COMPOSE up -d --build -echo "waiting for http://localhost:${AURUM_WEB_PORT} ..." +echo "waiting for http://localhost:${AURUM_WEB_PORT}/api/health ..." +# /api/health, not just / — nginx (the `web` container) answers the bare root +# well before the backend has finished running migrations + seeding, and a +# request racing ahead of that gets nginx's own HTML error page back instead +# of JSON, which the first test to touch the API then fails to parse. for _ in $(seq 1 60); do - if curl -sf "http://localhost:${AURUM_WEB_PORT}" >/dev/null; then + if curl -sf "http://localhost:${AURUM_WEB_PORT}/api/health" >/dev/null; then echo "stack is up" break fi diff --git a/frontend/package.json b/frontend/package.json index c880cef..03be0e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "aurum-frontend", "private": true, - "version": "0.19.2", + "version": "0.20.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/transactions/TransactionFormModal.tsx b/frontend/src/components/transactions/TransactionFormModal.tsx index 0462c31..04ebade 100644 --- a/frontend/src/components/transactions/TransactionFormModal.tsx +++ b/frontend/src/components/transactions/TransactionFormModal.tsx @@ -233,6 +233,15 @@ export function TransactionFormModal({ open, onClose, transaction }: Transaction /> +
+ + setForm((prev) => ({ ...prev, notes: event.target.value }))} + /> +
+ {error &&

{error}

}
diff --git a/frontend/src/components/transactions/TransactionsTable.tsx b/frontend/src/components/transactions/TransactionsTable.tsx index 8a58c8c..94b27b9 100644 --- a/frontend/src/components/transactions/TransactionsTable.tsx +++ b/frontend/src/components/transactions/TransactionsTable.tsx @@ -1,4 +1,6 @@ -import { ArrowLeftRight, Pencil, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { ArrowLeftRight, Pencil, StickyNote, Trash2 } from "lucide-react"; +import { Dialog } from "@/components/ui/Dialog"; import { getCategoryIcon } from "@/lib/icons"; import { formatCurrency, formatTransactionDate } from "@/lib/format"; import { useTranslation } from "@/lib/i18n"; @@ -13,67 +15,88 @@ interface TransactionsTableProps { export function TransactionsTable({ items, onEdit, onDelete }: TransactionsTableProps) { const { t } = useTranslation(); + const [noteTransaction, setNoteTransaction] = useState(null); if (items.length === 0) { return

{t("transactions.noneFound")}

; } return ( -
    - {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)"; + <> +
      + {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)"; - return ( -
    • - - - - - - {tx.description} - - {formatTransactionDate(tx.date)} · {tx.account.name} - {isTransfer && tx.transfer_account_id ? ` ${t("transactions.transferSuffix")}` : ""} - {tx.category ? ` · ${translateCategoryName(tx.category.name)}` : ""} + return ( +
    • + + - - - {isTransfer ? "" : isExpense ? "-" : "+"} - {formatCurrency(tx.amount)} - + + {tx.description} + + {formatTransactionDate(tx.date)} · {tx.account.name} + {isTransfer && tx.transfer_account_id ? ` ${t("transactions.transferSuffix")}` : ""} + {tx.category ? ` · ${translateCategoryName(tx.category.name)}` : ""} + + - - - - -
    • - ); - })} -
    + {isTransfer ? "" : isExpense ? "-" : "+"} + {formatCurrency(tx.amount)} + + + + {tx.notes && ( + + )} + + + + + ); + })} +
+ + setNoteTransaction(null)} + title={t("transactions.noteDialogTitle")} + > +

{noteTransaction?.notes}

+
+ ); } diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts index b3c576d..88ed006 100644 --- a/frontend/src/lib/i18n.ts +++ b/frontend/src/lib/i18n.ts @@ -160,6 +160,9 @@ const ru = { "transactions.form.categoryLabel": "Категория", "transactions.form.noCategory": "Без категории", "transactions.form.merchantLabel": "Продавец (необязательно)", + "transactions.form.notesLabel": "Заметка (необязательно)", + "transactions.viewNote": "Посмотреть заметку", + "transactions.noteDialogTitle": "Заметка", "transactions.form.errorSelectAccount": "Выберите счёт", "transactions.form.errorSelectDestination": "Выберите счёт назначения для перевода", "transactions.form.errorSameAccount": "Счёт назначения должен отличаться от исходного счёта", @@ -486,6 +489,9 @@ const en: Record = { "transactions.form.categoryLabel": "Category", "transactions.form.noCategory": "No category", "transactions.form.merchantLabel": "Merchant (optional)", + "transactions.form.notesLabel": "Note (optional)", + "transactions.viewNote": "View note", + "transactions.noteDialogTitle": "Note", "transactions.form.errorSelectAccount": "Select an account", "transactions.form.errorSelectDestination": "Select a destination account for the transfer", "transactions.form.errorSameAccount": "The destination account must be different from the source account",