Update spec: new two-column dashboard layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin 2026-03-19 23:06:15 +01:00
parent 74f924c715
commit 02e512a4a6

View File

@ -156,29 +156,38 @@ GET /api/transactions/categories
Returns 200: string[] sorted alphabetically Returns 200: string[] sorted alphabetically
GET /api/dashboard/summary GET /api/dashboard/summary
Query: year (required), month (required) Query: year (required), month (optional)
Returns 200: { totalSpent, totalIncome, currentBalance } — if month omitted: totals are for the entire year
— totalSpent: absolute value of sum of negative amounts WHERE booking_date — if month provided: totals are for that month only
falls within the given year+month Returns 200: { totalSpent, totalIncome }
— totalIncome: sum of positive amounts within the given year+month — totalSpent: absolute value of sum of negative amounts for the period
— currentBalance: balance from the transaction with the globally latest — totalIncome: sum of positive amounts for the period
booking_date (highest id as tiebreaker), ignoring year/month
filter; null if no transactions exist at all
GET /api/dashboard/spending-by-category 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 }] Returns 200: [{ category, total }]
— total: absolute value of sum of negative amounts for that category in period — total: absolute value of sum of negative amounts for that category in period
— positive amounts within any category are excluded from totals — positive amounts within any category are excluded from totals
— sorted descending by total — sorted descending by total
GET /api/dashboard/balance-trend GET /api/dashboard/monthly-balances
Query: year (required) Query: year (required)
Returns 200: [{ month, closingBalance }] Returns 200: [{ month, closingBalance }]
— one entry per month that has transactions (months with no transactions omitted) — one entry per month that has transactions (months with no transactions omitted)
— closingBalance: balance from the transaction with the latest booking_date — closingBalance: balance from the transaction with the latest booking_date
in that month (highest id as tiebreaker for same-date rows) 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 | | Login | `/login` | No |
| Dashboard | `/` | Yes | | Dashboard | `/` | Yes |
| Transactions | `/transactions` | 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`. 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 ### Dashboard Layout
Three KPI cards across the top (data from `/api/dashboard/summary`): **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.
- **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
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: **Left column — Year Summary**
- **Spending by Category** (ApexCharts horizontal bar) — category totals for selected month/year - Toolbar: previous-year button, selected year label, next-year button
- **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 - 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 ### Transactions Page
@ -219,14 +236,12 @@ Vuetify data table:
- Search box — server-side (`search` query param, debounced 300 ms) - Search box — server-side (`search` query param, debounced 300 ms)
- Server-side pagination (page, pageSize=50) - 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) ### State Management (Pinia)
- `auth` — token, expiresAt, login/logout actions, 401 handler - `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/ │ ├── src/
│ │ ├── api/ # NSwag-generated client (do not edit manually) │ │ ├── api/ # NSwag-generated client (do not edit manually)
│ │ ├── stores/ # Pinia: auth, period │ │ ├── stores/ # Pinia: auth, period
│ │ ├── views/ # Login, Dashboard, Transactions, Import │ │ ├── views/ # Login, Dashboard, Transactions
│ │ └── components/ # KpiCard, SpendingChart, BalanceTrendChart │ │ └── components/ # YearSummary, MonthSummary, DonutChart,
│ │ # MonthlyBalancesChart, CumulativeSpendingChart
│ ├── nswag.json # NSwag config (input: openapi.json, output: src/api/) │ ├── nswag.json # NSwag config (input: openapi.json, output: src/api/)
│ ├── vite.config.ts # includes /api proxy to http://localhost:5000 │ ├── vite.config.ts # includes /api proxy to http://localhost:5000
│ └── Dockerfile │ └── Dockerfile
@ -267,8 +283,10 @@ account-tracking/
- **Deduplication via `transaction_id`** — format `transhist-v-YYYY-MM_<sha>`; VARCHAR(512) utf8mb4_bin (case-sensitive) for correctness and future-proofing. - **Deduplication via `transaction_id`** — format `transhist-v-YYYY-MM_<sha>`; 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. - **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. - **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. - **Dashboard is two independent columns** — year summary (left) and month summary (right) each have their own period toolbar; they do not share state.
- **Balance trend omits months with no transactions** — ApexCharts handles sparse data; no zero-fill. - **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. - **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. - **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. - **NSwag via MSBuild**`openapi.json` generated at `dotnet build`; web Dockerfile copies it; no running API needed.