mirror of
https://github.com/Zproger/Aurum.git
synced 2026-11-10 17:57:22 +07:00
feat: view and edit a transaction's note from the list
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.
This commit is contained in:
@@ -6,7 +6,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|||||||
# Single source of truth for the running app's version — surfaced in the API
|
# 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
|
# title and embedded in exported backups so an old file can be told apart
|
||||||
# from a current one.
|
# from a current one.
|
||||||
APP_VERSION = "0.19.2"
|
APP_VERSION = "0.20.0"
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ trap cleanup EXIT
|
|||||||
|
|
||||||
$COMPOSE up -d --build
|
$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
|
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"
|
echo "stack is up"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "aurum-frontend",
|
"name": "aurum-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.19.2",
|
"version": "0.20.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -233,6 +233,15 @@ export function TransactionFormModal({ open, onClose, transaction }: Transaction
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="notes">{t("transactions.form.notesLabel")}</Label>
|
||||||
|
<Input
|
||||||
|
id="notes"
|
||||||
|
value={form.notes}
|
||||||
|
onChange={(event) => setForm((prev) => ({ ...prev, notes: event.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && <p className="text-sm text-danger">{error}</p>}
|
{error && <p className="text-sm text-danger">{error}</p>}
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
|||||||
@@ -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 { getCategoryIcon } from "@/lib/icons";
|
||||||
import { formatCurrency, formatTransactionDate } from "@/lib/format";
|
import { formatCurrency, formatTransactionDate } from "@/lib/format";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
@@ -13,12 +15,14 @@ interface TransactionsTableProps {
|
|||||||
|
|
||||||
export function TransactionsTable({ items, onEdit, onDelete }: TransactionsTableProps) {
|
export function TransactionsTable({ items, onEdit, onDelete }: TransactionsTableProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [noteTransaction, setNoteTransaction] = useState<Transaction | null>(null);
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return <p className="py-12 text-center text-sm text-text-muted">{t("transactions.noneFound")}</p>;
|
return <p className="py-12 text-center text-sm text-text-muted">{t("transactions.noneFound")}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<ul className="divide-y divide-gridline">
|
<ul className="divide-y divide-gridline">
|
||||||
{items.map((tx) => {
|
{items.map((tx) => {
|
||||||
const isTransfer = tx.type === "transfer";
|
const isTransfer = tx.type === "transfer";
|
||||||
@@ -54,6 +58,16 @@ export function TransactionsTable({ items, onEdit, onDelete }: TransactionsTable
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className="flex shrink-0 gap-1">
|
<span className="flex shrink-0 gap-1">
|
||||||
|
{tx.notes && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t("transactions.viewNote")}
|
||||||
|
onClick={() => setNoteTransaction(tx)}
|
||||||
|
className="rounded-md p-1.5 text-text-muted hover:bg-surface-2 hover:text-text-primary"
|
||||||
|
>
|
||||||
|
<StickyNote size={15} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={t("common.edit")}
|
aria-label={t("common.edit")}
|
||||||
@@ -75,5 +89,14 @@ export function TransactionsTable({ items, onEdit, onDelete }: TransactionsTable
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={noteTransaction !== null}
|
||||||
|
onClose={() => setNoteTransaction(null)}
|
||||||
|
title={t("transactions.noteDialogTitle")}
|
||||||
|
>
|
||||||
|
<p className="whitespace-pre-wrap text-sm text-text-primary">{noteTransaction?.notes}</p>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,6 +160,9 @@ const ru = {
|
|||||||
"transactions.form.categoryLabel": "Категория",
|
"transactions.form.categoryLabel": "Категория",
|
||||||
"transactions.form.noCategory": "Без категории",
|
"transactions.form.noCategory": "Без категории",
|
||||||
"transactions.form.merchantLabel": "Продавец (необязательно)",
|
"transactions.form.merchantLabel": "Продавец (необязательно)",
|
||||||
|
"transactions.form.notesLabel": "Заметка (необязательно)",
|
||||||
|
"transactions.viewNote": "Посмотреть заметку",
|
||||||
|
"transactions.noteDialogTitle": "Заметка",
|
||||||
"transactions.form.errorSelectAccount": "Выберите счёт",
|
"transactions.form.errorSelectAccount": "Выберите счёт",
|
||||||
"transactions.form.errorSelectDestination": "Выберите счёт назначения для перевода",
|
"transactions.form.errorSelectDestination": "Выберите счёт назначения для перевода",
|
||||||
"transactions.form.errorSameAccount": "Счёт назначения должен отличаться от исходного счёта",
|
"transactions.form.errorSameAccount": "Счёт назначения должен отличаться от исходного счёта",
|
||||||
@@ -486,6 +489,9 @@ const en: Record<keyof typeof ru, string> = {
|
|||||||
"transactions.form.categoryLabel": "Category",
|
"transactions.form.categoryLabel": "Category",
|
||||||
"transactions.form.noCategory": "No category",
|
"transactions.form.noCategory": "No category",
|
||||||
"transactions.form.merchantLabel": "Merchant (optional)",
|
"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.errorSelectAccount": "Select an account",
|
||||||
"transactions.form.errorSelectDestination": "Select a destination account for the transfer",
|
"transactions.form.errorSelectDestination": "Select a destination account for the transfer",
|
||||||
"transactions.form.errorSameAccount": "The destination account must be different from the source account",
|
"transactions.form.errorSameAccount": "The destination account must be different from the source account",
|
||||||
|
|||||||
Reference in New Issue
Block a user