diff --git a/docs/superpowers/specs/2026-03-19-finance-tracker-design.md b/docs/superpowers/specs/2026-03-19-finance-tracker-design.md index 18db22b..26af961 100644 --- a/docs/superpowers/specs/2026-03-19-finance-tracker-design.md +++ b/docs/superpowers/specs/2026-03-19-finance-tracker-design.md @@ -156,29 +156,38 @@ GET /api/transactions/categories Returns 200: string[] sorted alphabetically GET /api/dashboard/summary - Query: year (required), month (required) - Returns 200: { totalSpent, totalIncome, currentBalance } - — totalSpent: absolute value of sum of negative amounts WHERE booking_date - falls within the given year+month - — totalIncome: sum of positive amounts within the given year+month - — currentBalance: balance from the transaction with the globally latest - booking_date (highest id as tiebreaker), ignoring year/month - filter; null if no transactions exist at all + Query: year (required), month (optional) + — if month omitted: totals are for the entire year + — if month provided: totals are for that month only + Returns 200: { totalSpent, totalIncome } + — totalSpent: absolute value of sum of negative amounts for the period + — totalIncome: sum of positive amounts for the period GET /api/dashboard/spending-by-category - Query: year (required), month (required) + Query: year (required), month (optional) + — if month omitted: totals for the entire year + — if month provided: totals for that month only Returns 200: [{ category, total }] — total: absolute value of sum of negative amounts for that category in period — positive amounts within any category are excluded from totals — sorted descending by total -GET /api/dashboard/balance-trend +GET /api/dashboard/monthly-balances Query: year (required) Returns 200: [{ month, closingBalance }] — one entry per month that has transactions (months with no transactions omitted) — closingBalance: balance from the transaction with the latest booking_date in that month (highest id as tiebreaker for same-date rows) - — always year-scoped; ignores any month selection + — always year-scoped + +GET /api/dashboard/cumulative-spending + Query: year (required), month (required) + Returns 200: [{ day, cumulativeSpent }] + — one entry per day that has outgoing transactions within the given month + — cumulativeSpent: running absolute total of negative amounts from day 1 + through that day (sorted ascending by day) + — days with no spending are included with the previous day's cumulative value + so the line chart has no gaps ``` --- @@ -192,24 +201,32 @@ GET /api/dashboard/balance-trend | Login | `/login` | No | | Dashboard | `/` | Yes | | Transactions | `/transactions` | Yes | -| Import | `/import` | Yes | JWT and `expiresAt` stored in `localStorage`. Vue Router navigation guard checks local expiry and redirects unauthenticated/expired users to `/login`. On 401 API response, auth store clears storage and redirects to `/login`. ### Dashboard Layout -Three KPI cards across the top (data from `/api/dashboard/summary`): -- **Total Spent** — absolute value of negative amounts for selected month/year -- **Total Income** — sum of positive amounts for selected month/year -- **Current Balance** — globally most recent balance (not period-filtered); shows `—` if null +**App bar** — spans full width at top. Contains app title on the left and an "Upload CSV" button on the right. Clicking Upload CSV opens a file picker dialog; the selected file is uploaded via `POST /api/transactions/import` with an inline result snackbar ("Imported X, skipped Y" or error message). No separate import page. -Cards show a skeleton loader while fetching and an error state (red border, retry button) on failure. +**Two-column body** — equal-width columns side by side below the app bar. -Two charts below, side by side: -- **Spending by Category** (ApexCharts horizontal bar) — category totals for selected month/year -- **Balance Trend** (ApexCharts line) — closing balance per month for selected year; chart title includes year (e.g. "Balance trend — 2025") to communicate it is not filtered by month +**Left column — Year Summary** +- Toolbar: previous-year button, selected year label, next-year button +- Summary row: Total Expenses (red) | Total Income (green) — year-only totals (see API note below) +- Two sub-columns: + - **Doughnut chart** (ApexCharts) — expenses by category for the selected year; legend with category name and amount + - **Monthly balances bar chart** (ApexCharts) — closing balance per month for the selected year, from `/api/dashboard/monthly-balances` -Period picker (month + year dropdowns) in the app bar updates KPI cards and spending chart. Balance trend updates only when the year changes. +**Right column — Month Summary** +- Toolbar: previous-month button, selected month+year label, next-month button (wraps year when crossing Jan/Dec boundary) +- Summary row: Total Expenses (red) | Total Income (green) — from `/api/dashboard/summary` (year+month) +- Two sub-columns: + - **Doughnut chart** (ApexCharts) — expenses by category for the selected month; legend with category name and amount + - **Cumulative spending line chart** (ApexCharts) — running total of expenses day by day through the selected month, from `/api/dashboard/cumulative-spending` + +All widgets show a skeleton loader while fetching and a subtle error indicator on failure. + +The year selection (left toolbar) and month selection (right toolbar) are independent — changing one does not affect the other. ### Transactions Page @@ -219,14 +236,12 @@ Vuetify data table: - Search box — server-side (`search` query param, debounced 300 ms) - Server-side pagination (page, pageSize=50) -### Import Page - -File input (`.csv` only), upload button. On success: "Imported X transactions, skipped Y duplicates." On error: shows the `error` string from the API response. - ### State Management (Pinia) - `auth` — token, expiresAt, login/logout actions, 401 handler -- `period` — selected year/month, shared across dashboard and transactions +- `dashYear` — selected year for the year summary column +- `dashMonth` / `dashMonthYear` — selected month+year for the month summary column +- `txPeriod` — selected year/month for the transactions page --- @@ -248,8 +263,9 @@ account-tracking/ │ ├── src/ │ │ ├── api/ # NSwag-generated client (do not edit manually) │ │ ├── stores/ # Pinia: auth, period -│ │ ├── views/ # Login, Dashboard, Transactions, Import -│ │ └── components/ # KpiCard, SpendingChart, BalanceTrendChart +│ │ ├── views/ # Login, Dashboard, Transactions +│ │ └── components/ # YearSummary, MonthSummary, DonutChart, +│ │ # MonthlyBalancesChart, CumulativeSpendingChart │ ├── nswag.json # NSwag config (input: openapi.json, output: src/api/) │ ├── vite.config.ts # includes /api proxy to http://localhost:5000 │ └── Dockerfile @@ -267,8 +283,10 @@ account-tracking/ - **Deduplication via `transaction_id`** — format `transhist-v-YYYY-MM_`; VARCHAR(512) utf8mb4_bin (case-sensitive) for correctness and future-proofing. - **CSV header detection** — scan for line starting with `číslo účtu`; do not hardcode line number. - **Spending totals exclude positive amounts per category** — refunds within a spending category are excluded from totals. -- **`currentBalance` is not period-filtered** — always the globally latest transaction balance; clearly documented in API spec and shown as "not filtered" in UI. -- **Balance trend omits months with no transactions** — ApexCharts handles sparse data; no zero-fill. +- **Dashboard is two independent columns** — year summary (left) and month summary (right) each have their own period toolbar; they do not share state. +- **Upload CSV in app bar** — file picker dialog in-place, no separate import page; result shown as snackbar. +- **Month summary line chart is cumulative spending** — running total of expenses day by day through the selected month; gaps filled so line is continuous. +- **Monthly balances bar chart** — closing balance per month for the year (sparse, months with no data omitted). - **Search is contains-match (OR)** — `%term%` on counter_party_name OR message; case-insensitive via utf8mb4_unicode_ci. - **Server-side search and pagination** — no client-side filtering of paginated data. - **NSwag via MSBuild** — `openapi.json` generated at `dotnet build`; web Dockerfile copies it; no running API needed.