mitgliederverwaltung/docs/statistics-page-implementation-plan.md
Moritz 6ffd1ea5aa
All checks were successful
continuous-integration/drone/push Build is passing
Update docs and guidelines for statistics feature
- CODE_GUIDELINES.md and feature-roadmap.md
- Add statistics-page-implementation-plan.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 22:31:52 +01:00

9.5 KiB
Raw Blame History

Statistics Page Implementation Plan

Project: Mila Membership Management System
Feature: Statistics page at /statistics
Scope: MVP only (no export, no optional extensions)
Last updated: 2026-02-10


Decisions (from clarification)

Topic Decision
Route /statistics
Navigation Top-level menu (next to Members, Fee Types)
Permission read_only, normal_user, admin (same as member list)
Charts HTML/CSS and SVG only (no Contex, no Chart.js)
MVP scope Minimal: active/inactive, joins/exits per year, contribution sums per year, open amount
Open amount Total unpaid only (no overdue vs. not-yet-due split in MVP)

Excluded from this plan: Export (CSV/PDF), caching, month/quarter filters, “members per fee type”, “members per group”, and overdue split.


1. Statistics module (Mv.Statistics)

Goal: Central module for all statistics; LiveView only calls this API. Uses Ash reads with actor so policies apply.

Location: lib/mv/statistics.ex (new).

Functions to implement:

Function Purpose Data source
active_member_count(opts) Count members with exit_date == nil Member read with filter
inactive_member_count(opts) Count members with exit_date != nil Member read with filter
joins_by_year(year, opts) Count members with join_date in given year Member read, filter by year, count
exits_by_year(year, opts) Count members with exit_date in given year Member read, filter by year, count
cycle_totals_by_year(year, opts) For cycles with cycle_start in year: total sum, and sums/counts by status (paid, unpaid, suspended) MembershipFeeCycle read (filter by year via cycle_start), aggregate sum(amount) and count per status in Elixir or via Ash aggregates
open_amount_total(opts) Sum of amount for all cycles with status == :unpaid MembershipFeeCycle read with filter status == :unpaid, sum(amount)

All functions accept opts (keyword list) and pass actor: opts[:actor] (and domain: where needed) to Ash calls. No new resources; only read actions on existing Member and MembershipFeeCycle.

Implementation notes:

  • Use Ash.Query.filter(Member, expr(...)) for date filters; for “year”, filter join_date >= first_day_of_year and join_date <= last_day_of_year (same for exit_date and for MembershipFeeCycle.cycle_start).
  • For cycle_totals_by_year: either multiple Ash reads (one per status) with sum aggregate, or one read of cycles in that year and Enum.group_by(..., :status) then sum amounts in Elixir.
  • Use Mv.MembershipFees.CalendarCycles only if needed for interval (e.g. cycle_end); for “cycle in year” the cycle_start year is enough.

Tests: Unit tests in test/mv/statistics_test.exs for each function (with fixtures: members with join_date/exit_date, cycles with cycle_start/amount/status). Use Mv.Helpers.SystemActor.get_system_actor() in tests for Ash read authorization where appropriate.


2. Route and authorization

Router (lib/mv_web/router.ex):

  • In the same ash_authentication_live_session block where /members and /membership_fee_types live, add:
    • live "/statistics", StatisticsLive, :index

PagePaths (lib/mv_web/page_paths.ex):

  • Add module attribute @statistics "/statistics".
  • Add def statistics, do: @statistics.
  • No change to @admin_page_paths (statistics is top-level).

Page permission (route matrix is driven by lib/mv/authorization/permission_sets.ex):

  • Add "/statistics" to the pages list of read_only (e.g. after "/groups/:slug") and to the pages list of normal_user (e.g. after groups entries). admin already has "*" so no change.
  • own_data must not list /statistics (so they cannot access it).
  • Update docs/page-permission-route-coverage.md: add row for | /statistics | ✗ | ✓ | ✓ | ✓ |.
  • Add test in test/mv_web/plugs/check_page_permission_test.exs: read_only and normal_user and admin can access /statistics; own_data cannot.

3. Sidebar

File: lib/mv_web/components/layouts/sidebar.ex.

  • In sidebar_menu, after the “Fee Types” menu item and before the “Administration” block, add a conditional menu item for Statistics:
    • can_access_page?(@current_user, PagePaths.statistics()) → show link.
    • href={~p"/statistics"}, icon="hero-chart-bar" (or similar), label={gettext("Statistics")}.

4. Statistics LiveView

Module: MvWeb.StatisticsLive
File: lib/mv_web/live/statistics_live.ex
Mount: :index only.

Behaviour:

  • on_mount: use MvWeb.LiveUserAuth, :live_user_required and ensure role/permission check (same as other protected LiveViews). In mount or handle_params, set default selected year to current year (e.g. Date.utc_today().year).
  • Assigns: :year (integer), :active_count, :inactive_count, :joins_this_year, :exits_this_year, :cycle_totals (map with keys e.g. :total, :paid, :unpaid, :suspended for the selected year), :open_amount_total, and any extra needed for the bar data (e.g. list of %{year: y, joins: j, exits: e} for a small range of years if you show a minimal bar chart).
  • Year filter: A single select or dropdown for year (e.g. from “first year with data” to current year). On change, send event (e.g. "set_year") with %{"year" => year}; in handle_event update assigns.year and reload data by calling Mv.Statistics again and re-assigning.

Data loading:

  • In mount and whenever year changes, call Mv.Statistics with actor: current_actor(socket) (and optionally year: @year where needed). Assign results to socket. Handle errors (e.g. redirect or flash) if a call fails.

Layout (sections):

  1. Page title: e.g. “Statistics” (gettext).
  2. Year filter: One control to select year; applies to “joins/exits” and “contribution sums” for that year.
  3. Cards (top row):
    • Active members (count)
    • Inactive members (count)
    • Joins in selected year
    • Exits in selected year
    • Open amount total (sum of all unpaid cycles; format with MvWeb.Helpers.MembershipFeeHelpers.format_currency/1)
    • Optionally: “Paid this year” (from cycle_totals_by_year for selected year)
  4. Contributions for selected year: One section showing for the chosen year: total (Soll), paid, unpaid, suspended (sums and optionally counts). Use simple table or key-value list; no chart required for MVP.
  5. Joins / Exits by year (simple bar chart): Data: e.g. last 5 or 10 years. For each year, show joins and exits as horizontal bars (HTML/CSS: e.g. div with width: #{percent}%). Pure HTML/SVG; no external chart library. Use Tailwind/DaisyUI for layout and cards.

Accessibility: Semantic HTML; headings (e.g. h2) for each section; ensure year filter has a label; format numbers in a screen-reader-friendly way (e.g. no purely visual abbreviations without aria-label).

i18n: All user-visible strings via gettext (e.g. “Statistics”, “Active members”, “Inactive members”, “Joins (year)”, “Exits (year)”, “Open amount”, “Contributions for year”, “Total”, “Paid”, “Unpaid”, “Suspended”). Add keys to priv/gettext as needed.


5. Implementation order (tasks)

Execute in this order so that each step is testable:

  1. Statistics module

    • Add lib/mv/statistics.ex with the six functions above and @moduledoc.
    • Add test/mv/statistics_test.exs with tests for each function (use fixtures for members and cycles; pass actor in opts).
    • Run tests and fix until green.
  2. Route and permission

    • Add live "/statistics", StatisticsLive, :index in router.
    • Add statistics/0 and @statistics in PagePaths.
    • Add /statistics to page permission logic so read_only, normal_user, admin are allowed and own_data is denied.
    • Update docs/page-permission-route-coverage.md and add/update plug tests for /statistics.
  3. Sidebar

    • Add Statistics link in sidebar (top-level) with can_access_page? and PagePaths.statistics().
  4. StatisticsLive

    • Create lib/mv_web/live/statistics_live.ex with mount, assigns, year param, and data loading from Mv.Statistics.
    • Implement UI: title, year filter, cards, contribution section, simple joins/exits bar (HTML).
    • Add gettext keys and use them in the template.
    • Optionally add a simple LiveView test (e.g. authenticated user sees statistics page and key labels).
  5. CI and docs

    • Run just ci-dev (or project equivalent); fix formatting, Credo, and tests.
    • In docs/feature-roadmap.md, update “Reporting & Analytics” to reflect that a basic statistics page is implemented (MVP).
    • In CODE_GUIDELINES.md, add a short note under a suitable section (e.g. “Reporting” or “LiveView”) that statistics are provided by Mv.Statistics and displayed in StatisticsLive, if desired.

6. Out of scope (not in this plan)

  • Export (CSV/PDF).
  • Caching (ETS/GenServer/HTTP).
  • Month or quarter filters.
  • “Members per fee type” or “members per group” statistics.
  • Overdue vs. not-yet-due split for open amount.
  • Contex or Chart.js.
  • New database tables or Ash resources.

These can be added later as separate tasks or follow-up plans.