Merge branch 'main' into feature/286_export_pdf
This commit is contained in:
commit
22458cd52b
34 changed files with 4931 additions and 76 deletions
|
|
@ -291,10 +291,10 @@
|
|||
#### 10. **Reporting & Analytics** 📊
|
||||
|
||||
**Current State:**
|
||||
- ❌ No reporting features
|
||||
- ✅ **Statistics page (MVP)** – `/statistics` with active/inactive member counts, joins/exits by year, cycle totals, open amount (2026-02-10)
|
||||
|
||||
**Missing Features:**
|
||||
- ❌ Member statistics dashboard
|
||||
- ❌ Extended member statistics dashboard
|
||||
- ❌ Membership growth charts
|
||||
- ❌ Payment reports
|
||||
- ❌ Custom report builder
|
||||
|
|
|
|||
|
|
@ -314,9 +314,24 @@ lib/
|
|||
- Display group name and description
|
||||
- List all members in group
|
||||
- Link to member detail pages
|
||||
- Add members to group (via inline combobox with search/autocomplete)
|
||||
- Remove members from group (via remove button per member)
|
||||
- Edit group button (navigates to `/groups/:slug/edit`)
|
||||
- Delete group button (with confirmation modal)
|
||||
|
||||
**Add Member Functionality:**
|
||||
- "Add Member" button displayed above member table (only for users with `:update` permission)
|
||||
- Opens inline add member area with member search/autocomplete (combobox)
|
||||
- Search filters out members already in the group
|
||||
- Selecting a member adds them to the group immediately
|
||||
- Success/error flash messages provide feedback
|
||||
- "Cancel" button closes the inline add member area without adding
|
||||
|
||||
**Remove Member Functionality:**
|
||||
- "Remove" button (icon button) for each member in table (only for users with `:update` permission)
|
||||
- Clicking remove immediately removes member from group (no confirmation dialog)
|
||||
- Success/error flash messages provide feedback
|
||||
|
||||
**Note:** Uses slug for routing to provide URL-friendly, readable group URLs (e.g., `/groups/board-members`).
|
||||
|
||||
### Group Form Pages
|
||||
|
|
@ -754,6 +769,7 @@ Each functional unit can be implemented as a **separate issue**:
|
|||
- **Issue 4:** Groups in Member Detail (Unit 5)
|
||||
- **Issue 5:** Groups in Member Search (Unit 6)
|
||||
- **Issue 6:** Permissions (Unit 7)
|
||||
- **Issue 7:** Add/Remove Members in Group Detail View
|
||||
|
||||
**Alternative:** Issues 3 and 4 can be combined, as they both concern the display of groups.
|
||||
|
||||
|
|
@ -799,6 +815,27 @@ Each functional unit can be implemented as a **separate issue**:
|
|||
|
||||
**Estimation:** 3-4h
|
||||
|
||||
### Phase 2a: Add/Remove Members in Group Detail View
|
||||
|
||||
**Goal:** Enable adding and removing members from groups via UI
|
||||
|
||||
**Tasks:**
|
||||
1. Add "Add Member" button above member table in Group Detail View
|
||||
2. Implement inline add member with search/autocomplete
|
||||
3. Add "Remove" button for each member in table
|
||||
4. Implement add/remove functionality with flash messages
|
||||
5. Ensure proper authorization checks
|
||||
|
||||
**Deliverables:**
|
||||
- Members can be added to groups via UI
|
||||
- Members can be removed from groups via UI
|
||||
- Proper feedback via flash messages
|
||||
- Authorization enforced
|
||||
|
||||
**Estimation:** 2-3h
|
||||
|
||||
**Note:** This phase extends Phase 2 and can be implemented as Issue 7 after Issue 2 is complete.
|
||||
|
||||
### Phase 3: Member Overview Integration
|
||||
|
||||
**Goal:** Display and filter groups in member overview
|
||||
|
|
@ -865,9 +902,9 @@ Each functional unit can be implemented as a **separate issue**:
|
|||
|
||||
**Estimation:** 1-2h
|
||||
|
||||
### Total Estimation: 13-18h
|
||||
### Total Estimation: 15-21h
|
||||
|
||||
**Note:** This aligns with the issue estimation of 15h.
|
||||
**Note:** This includes all 7 issues. The original MVP estimation was 13-15h, with Issue 7 adding 2-3h for the add/remove members functionality in the Group Detail View.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -960,6 +997,55 @@ Each functional unit can be implemented as a **separate issue**:
|
|||
- Only admins can manage groups
|
||||
- All users can view groups (if they can view members)
|
||||
|
||||
### Issue 7: Add/Remove Members in Group Detail View
|
||||
**Type:** Frontend
|
||||
**Estimation:** 2-3h
|
||||
**Dependencies:** Issue 1 (Backend must be functional), Issue 2 (Group Detail View must exist)
|
||||
|
||||
**Tasks:**
|
||||
- Add "Add Member" button above member table in Group Detail View (`/groups/:slug`)
|
||||
- Implement inline add member for member selection with search/autocomplete
|
||||
- Add "Remove" button for each member in the member table
|
||||
- Implement add member functionality (create MemberGroup association)
|
||||
- Implement remove member functionality (destroy MemberGroup association)
|
||||
- Add flash messages for success/error feedback
|
||||
- Ensure proper authorization checks (only users with `:update` permission on Group can add/remove)
|
||||
- Filter out members already in the group from search results
|
||||
- Reload group data after add/remove operations
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- "Add Member" button is visible above member table (only for users with `:update` permission)
|
||||
- Clicking "Add Member" opens inline add member area with member search/autocomplete
|
||||
- Search filters members and excludes those already in the group
|
||||
- Selecting a member from search adds them to the group
|
||||
- Success flash message is displayed when member is added
|
||||
- Error flash message is displayed if member is already in group or other error occurs
|
||||
- Each member row in the table has a "Remove" button (only visible for users with `:update` permission)
|
||||
- Clicking "Remove" immediately removes the member from the group (no confirmation dialog)
|
||||
- Success flash message is displayed when member is removed
|
||||
- Group member list and member count update automatically after add/remove
|
||||
- Inline add member area closes after successful member addition
|
||||
- Authorization is enforced server-side in event handlers
|
||||
- UI respects permission checks (buttons hidden for unauthorized users)
|
||||
|
||||
**Technical Notes:**
|
||||
- Reuse member search pattern from `UserLive.Form` (ComboBox hook with autocomplete)
|
||||
- Use `Membership.create_member_group/1` for adding members
|
||||
- Use `Membership.destroy_member_group/1` for removing members
|
||||
- Filter search results to exclude members already in the group (check `group.members`)
|
||||
- Reload group with `:members` and `:member_count` after operations
|
||||
- Use inline combobox pattern (delete group uses a separate confirmation modal)
|
||||
- Ensure accessibility: proper ARIA labels, keyboard navigation, focus management
|
||||
|
||||
**UI/UX Details:**
|
||||
- Inline add member section (no modal; combobox above member table)
|
||||
- Search input placeholder: "Search for a member..."
|
||||
- Search results show member name and email
|
||||
- "Add" button in inline area (disabled until member selected)
|
||||
- "Cancel" button to close inline add member area
|
||||
- Remove button can be an icon button (trash icon) with tooltip
|
||||
- Flash messages: "Member added successfully" / "Member removed successfully" / error messages
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ This document lists all protected routes, which permission set may access them,
|
|||
| `/groups/new` | ✗ | ✗ | ✗ | ✓ |
|
||||
| `/groups/:slug` | ✗ | ✓ | ✓ | ✓ |
|
||||
| `/groups/:slug/edit` | ✗ | ✗ | ✗ | ✓ |
|
||||
| `/statistics` | ✗ | ✓ | ✓ | ✓ |
|
||||
| `/admin/roles` | ✗ | ✗ | ✗ | ✓ |
|
||||
| `/admin/roles/new` | ✗ | ✗ | ✗ | ✓ |
|
||||
| `/admin/roles/:id` | ✗ | ✗ | ✗ | ✓ |
|
||||
|
|
|
|||
163
docs/statistics-page-implementation-plan.md
Normal file
163
docs/statistics-page-implementation-plan.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# 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](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](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](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](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](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](docs/feature-roadmap.md), update “Reporting & Analytics” to reflect that a basic statistics page is implemented (MVP).
|
||||
- In [CODE_GUIDELINES.md](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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue