diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c23c01..28b4a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,27 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- **Roles and Permissions System (RBAC)** - Complete implementation (#345, 2026-01-08) - - Four hardcoded permission sets: `own_data`, `read_only`, `normal_user`, `admin` - - Database-backed roles with permission set references - - Member resource policies with scope filtering (`:own`, `:linked`, `:all`) - - Authorization checks via `Mv.Authorization.Checks.HasPermission` - - System role protection (critical roles cannot be deleted) - - Role management UI at `/admin/roles` -- **Membership Fees System** - Full implementation - - Membership fee types with intervals (monthly, quarterly, half_yearly, yearly) - - Individual billing cycles per member with payment status tracking - - Cycle generation and regeneration - - Global membership fee settings - - UI components for fee management -- **Global Settings Management** - Singleton settings resource - - Club name configuration (with environment variable support) - - Member field visibility settings - - Membership fee default settings -- **Sidebar Navigation** - Replaced navbar with standard-compliant sidebar (#260, 2026-01-12) -- **CSV Import Templates** - German and English templates (#329, 2026-01-13) - - Template files in `priv/static/templates/` - - CSV specification documented - User-Member linking with fuzzy search autocomplete (#168) - PostgreSQL trigram-based member search with typo tolerance - WCAG 2.1 AA compliant autocomplete dropdown with ARIA support @@ -40,22 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - German/English translations - Docker secrets support via `_FILE` environment variables for all sensitive configuration (SECRET_KEY_BASE, TOKEN_SIGNING_SECRET, OIDC_CLIENT_SECRET, DATABASE_URL, DATABASE_PASSWORD) -### Changed -- **Actor Handling Refactoring** (2026-01-09) - - Standardized actor access with `current_actor/1` helper function - - `ash_actor_opts/1` helper for consistent authorization options - - `submit_form/3` wrapper for form submissions with actor - - All Ash operations now properly pass `actor` parameter -- **Error Handling Improvements** (2026-01-13) - - Replaced `Ash.read!` with proper error handling in LiveViews - - Consistent flash message handling for authorization errors - - Early return patterns for unauthenticated users - ### Fixed - Email validation false positive when linking user and member with identical emails (#168 Problem #4) - Relationship data extraction from Ash manage_relationship during validation - Copy button count now shows only visible selected members when filtering -- Language headers in German `.po` files (corrected from "en" to "de") -- Critical deny-filter bug in authorization system (2026-01-08) -- HasPermission auto_filter and strict_check implementation (2026-01-08) diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md index fe2d816..5cc792c 100644 --- a/CODE_GUIDELINES.md +++ b/CODE_GUIDELINES.md @@ -83,18 +83,7 @@ lib/ │ ├── member.ex # Member resource │ ├── custom_field_value.ex # Custom field value resource │ ├── custom_field.ex # CustomFieldValue type resource -│ ├── setting.ex # Global settings (singleton resource) │ └── email.ex # Email custom type -├── membership_fees/ # MembershipFees domain -│ ├── membership_fees.ex # Domain definition -│ ├── membership_fee_type.ex # Membership fee type resource -│ ├── membership_fee_cycle.ex # Membership fee cycle resource -│ └── changes/ # Ash changes for membership fees -├── mv/authorization/ # Authorization domain -│ ├── authorization.ex # Domain definition -│ ├── role.ex # Role resource -│ ├── permission_sets.ex # Hardcoded permission sets -│ └── checks/ # Authorization checks ├── mv/ # Core application modules │ ├── accounts/ # Domain-specific logic │ │ └── user/ @@ -107,11 +96,6 @@ lib/ │ ├── membership/ # Domain-specific logic │ │ └── member/ │ │ └── validations/ -│ ├── membership_fees/ # Membership fee business logic -│ │ ├── cycle_generator.ex # Cycle generation algorithm -│ │ └── calendar_cycles.ex # Calendar cycle calculations -│ ├── helpers.ex # Shared helper functions (ash_actor_opts) -│ ├── constants.ex # Application constants (member_fields, custom_field_prefix) │ ├── application.ex # OTP application │ ├── mailer.ex # Email mailer │ ├── release.ex # Release tasks @@ -123,7 +107,7 @@ lib/ │ │ ├── table_components.ex │ │ ├── layouts.ex │ │ └── layouts/ # Layout templates -│ │ ├── sidebar.ex +│ │ ├── navbar.ex │ │ └── root.html.heex │ ├── controllers/ # HTTP controllers │ │ ├── auth_controller.ex @@ -132,11 +116,6 @@ lib/ │ │ ├── error_html.ex │ │ ├── error_json.ex │ │ └── page_html/ -│ ├── helpers/ # Web layer helper modules -│ │ ├── member_helpers.ex # Member display utilities -│ │ ├── membership_fee_helpers.ex # Membership fee formatting -│ │ ├── date_formatter.ex # Date formatting utilities -│ │ └── field_type_formatter.ex # Field type display formatting │ ├── live/ # LiveView modules │ │ ├── components/ # LiveView-specific components │ │ │ ├── search_bar_component.ex @@ -144,16 +123,11 @@ lib/ │ │ ├── member_live/ # Member CRUD LiveViews │ │ ├── custom_field_value_live/ # CustomFieldValue CRUD LiveViews │ │ ├── custom_field_live/ -│ │ ├── user_live/ # User management LiveViews -│ │ ├── role_live/ # Role management LiveViews -│ │ ├── membership_fee_type_live/ # Membership fee type LiveViews -│ │ ├── membership_fee_settings_live.ex # Membership fee settings -│ │ ├── global_settings_live.ex # Global settings -│ │ └── contribution_type_live/ # Contribution types (mock-up) +│ │ └── user_live/ # User management LiveViews │ ├── auth_overrides.ex # AshAuthentication overrides │ ├── endpoint.ex # Phoenix endpoint │ ├── gettext.ex # I18n configuration -│ ├── live_helpers.ex # LiveView lifecycle hooks and helpers +│ ├── live_helpers.ex # LiveView helpers │ ├── live_user_auth.ex # LiveView authentication │ ├── router.ex # Application router │ └── telemetry.ex # Telemetry configuration @@ -202,7 +176,7 @@ test/ **Module Naming:** - **Modules:** Use `PascalCase` with full namespace (e.g., `Mv.Accounts.User`) -- **Domains:** Top-level domains are `Mv.Accounts`, `Mv.Membership`, `Mv.MembershipFees`, and `Mv.Authorization` +- **Domains:** Top-level domains are `Mv.Accounts` and `Mv.Membership` - **Resources:** Resource modules should be singular nouns (e.g., `Member`, not `Members`) - **Context functions:** Use `snake_case` and verb-first naming (e.g., `create_user`, `list_members`) @@ -844,17 +818,14 @@ end ```heex - -
- -
- + +``` + +### B. CSS Selector Beispiele + +```css +/* Expanded (default) */ +.sidebar { width: 16rem; } +.menu-label { opacity: 1; } +.expanded-only { display: block; } + +/* Collapsed */ +[data-sidebar-expanded="false"] .sidebar { width: 4rem; } +[data-sidebar-expanded="false"] .sidebar .menu-label { opacity: 0; } +[data-sidebar-expanded="false"] .sidebar .expanded-only { display: none; } +``` + +### C. localStorage Beispiel + +```javascript +// Save +localStorage.setItem('sidebar-expanded', 'true'); + +// Load +const expanded = localStorage.getItem('sidebar-expanded') !== 'false'; + +// Check +if (localStorage.getItem('sidebar-expanded') === 'false') { + // Collapsed +} +``` + +### D. ARIA Beispiele + +```html + + +``` + +--- + +**Ende der Spezifikation** + + + diff --git a/docs/test-failures-analysis.md b/docs/test-failures-analysis.md new file mode 100644 index 0000000..d105b90 --- /dev/null +++ b/docs/test-failures-analysis.md @@ -0,0 +1,233 @@ +# Analyse der fehlschlagenden Tests + +## Übersicht + +**Gesamtanzahl fehlschlagender Tests:** 5 +- **show_test.exs:** 1 Fehler +- **sidebar_test.exs:** 4 Fehler + +--- + +## Kategorisierung + +### Kategorie 1: Test-Assertions passen nicht zur Implementierung (4 Tests) + +Diese Tests erwarten bestimmte Werte/Attribute, die in der aktuellen Implementierung anders sind oder fehlen. + +### Kategorie 2: Datenbank-Isolation Problem (1 Test) + +Ein Test schlägt fehl, weil die Datenbank nicht korrekt isoliert ist. + +--- + +## Detaillierte Analyse + +### 1. `show_test.exs` - Custom Fields Sichtbarkeit + +**Test:** `does not display Custom Fields section when no custom fields exist` (Zeile 112) + +**Problem:** +- Der Test erwartet, dass die "Custom Fields" Sektion NICHT angezeigt wird, wenn keine Custom Fields existieren +- Die Sektion wird aber angezeigt, weil in der Datenbank noch Custom Fields von anderen Tests vorhanden sind + +**Ursache:** +- Die LiveView lädt alle Custom Fields aus der Datenbank (Zeile 238-242 in `show.ex`) +- Die Test-Datenbank wird nicht zwischen Tests geleert +- Da `async: false` verwendet wird, sollten die Tests sequenziell laufen, aber Custom Fields bleiben in der Datenbank + +**Kategorie:** Datenbank-Isolation Problem + +--- + +### 2. `sidebar_test.exs` - Settings Link + +**Test:** `T3.1: renders flat menu items with icons and labels` (Zeile 174) + +**Problem:** +- Test erwartet `href="#"` für Settings +- Tatsächlicher Wert: `href="/settings"` + +**Ursache:** +- Die Implementierung verwendet einen echten Link `~p"/settings"` (Zeile 100 in `sidebar.ex`) +- Der Test erwartet einen Placeholder-Link `href="#"` + +**Kategorie:** Test-Assertion passt nicht zur Implementierung + +--- + +### 3. `sidebar_test.exs` - Drawer Overlay CSS-Klasse + +**Test:** `drawer overlay is present` (Zeile 747) + +**Problem:** +- Test sucht nach exakt `class="drawer-overlay"` +- Tatsächlicher Wert: `class="drawer-overlay lg:hidden focus:outline-none focus:ring-2 focus:ring-primary"` + +**Ursache:** +- Der Test verwendet eine exakte String-Suche (`~s(class="drawer-overlay")`) +- Die Implementierung hat mehrere CSS-Klassen + +**Kategorie:** Test-Assertion passt nicht zur Implementierung + +--- + +### 4. `sidebar_test.exs` - Toggle Button ARIA-Attribut + +**Test:** `T5.2: toggle button has correct ARIA attributes` (Zeile 324) + +**Problem:** +- Test erwartet `aria-controls="main-sidebar"` am Toggle-Button +- Das Attribut fehlt in der Implementierung (Zeile 45-65 in `sidebar.ex`) + +**Ursache:** +- Das `aria-controls` Attribut wurde nicht in der Implementierung hinzugefügt +- Der Test erwartet es für bessere Accessibility + +**Kategorie:** Test-Assertion passt nicht zur Implementierung (Accessibility-Feature fehlt) + +--- + +### 5. `sidebar_test.exs` - Contribution Settings Link + +**Test:** `sidebar structure is complete with all sections` (Zeile 501) + +**Problem:** +- Test erwartet Link `/contribution_settings` +- Tatsächlicher Link: `/membership_fee_settings` + +**Ursache:** +- Der Test hat eine veraltete/inkorrekte Erwartung +- Die Implementierung verwendet `/membership_fee_settings` (Zeile 96 in `sidebar.ex`) + +**Kategorie:** Test-Assertion passt nicht zur Implementierung (veralteter Test) + +--- + +## Lösungsvorschläge + +### Lösung 1: `show_test.exs` - Custom Fields Sichtbarkeit + +**Option A: Test-Datenbank bereinigen (Empfohlen)** +- Im `setup` Block alle Custom Fields löschen, bevor der Test läuft +- Oder: Explizit prüfen, dass keine Custom Fields existieren + +**Option B: Test anpassen** +- Den Test so anpassen, dass er explizit alle Custom Fields löscht +- Oder: Die LiveView-Logik ändern, um nur Custom Fields zu laden, die tatsächlich existieren + +**Empfehlung:** Option A - Im Test-Setup alle Custom Fields löschen + +```elixir +setup do + # Clean up any existing custom fields + Mv.Membership.CustomField + |> Ash.read!() + |> Enum.each(&Ash.destroy!/1) + + # Create test member + {:ok, member} = ... + %{member: member} +end +``` + +--- + +### Lösung 2: `sidebar_test.exs` - Settings Link + +**Option A: Test anpassen (Empfohlen)** +- Test ändern, um `href="/settings"` zu erwarten statt `href="#"` + +**Option B: Implementierung ändern** +- Settings-Link zu `href="#"` ändern (nicht empfohlen, da es ein echter Link sein sollte) + +**Empfehlung:** Option A - Test anpassen + +```elixir +# Zeile 190 ändern von: +assert html =~ ~s(href="#") +# zu: +assert html =~ ~s(href="/settings") +``` + +--- + +### Lösung 3: `sidebar_test.exs` - Drawer Overlay CSS-Klasse + +**Option A: Test anpassen (Empfohlen)** +- Test ändern, um nach der Klasse in der Klasse-Liste zu suchen (mit `has_class?` Helper) + +**Option B: Regex verwenden** +- Regex verwenden, um die Klasse zu finden + +**Empfehlung:** Option A - Test anpassen + +```elixir +# Zeile 752 ändern von: +assert html =~ ~s(class="drawer-overlay") +# zu: +assert has_class?(html, "drawer-overlay") +``` + +--- + +### Lösung 4: `sidebar_test.exs` - Toggle Button ARIA-Attribut + +**Option A: Implementierung anpassen (Empfohlen)** +- `aria-controls="main-sidebar"` zum Toggle-Button hinzufügen + +**Option B: Test anpassen** +- Test entfernen oder als optional markieren (nicht empfohlen für Accessibility) + +**Empfehlung:** Option A - Implementierung anpassen + +```elixir +# In sidebar.ex Zeile 45-52, aria-controls hinzufügen: + + <% end %> +
+ """ +end +``` + +## Integration in layouts.ex: + +Ersetze den Sidebar Placeholder in `layouts.ex`: + +```heex + +``` + +## Testen: +1. Kompiliere: `mix compile` +2. Starte Server: `mix phx.server` +3. Prüfe: + - Logo ist immer 32px groß + - Toggle-Button erscheint nur auf Desktop + - Button-Design passt zum Rest der Sidebar + - Icon wechselt beim Toggle + - Hover-Effekt funktioniert + - ARIA-Attribute korrekt + - Club-Name verschwindet beim Collapse (wenn toggleSidebar() funktioniert) +``` + +### Acceptance Criteria: +- ✅ sidebar_header Komponente erstellt +- ✅ Logo immer gleich groß +- ✅ Toggle-Button nur auf Desktop +- ✅ DaisyUI-konforme Button-Klassen verwendet +- ✅ Icon-Swap funktioniert (egal welche Variante gewählt wurde) +- ✅ Event-Handler funktioniert +- ✅ Design fügt sich harmonisch ein +- ✅ Keine Layout-Breaks + +--- + +## Task 7: Sidebar Navigation - Flat Items + +**Agent:** Auto-Mode +**Geschätzte Dauer:** 25 Minuten + +### Prompt: + +``` +Implementiere einfache Menü-Items (flat, ohne Nesting) in `lib/mv_web/components/layouts/sidebar.ex`: + +## Menü-Items: +- Members (`/members`) +- Users (`/users`) +- Custom Fields (`/custom_fields`) +- Settings (Placeholder) + +## Anforderungen: + +1. **Expanded State:** + - Icon + Text-Label + - Standard DaisyUI menu hover + +2. **Collapsed State:**gf + - Nur Icon + - Tooltip erscheint rechts (`tooltip-right`) + - Tooltip-Text aus `data-tip` + +3. **Hover-Effekt:** + - Einheitlich (Standard DaisyUI menu) + - Keine custom hover-styles + +4. **Active State:** + - Highlight für current_path (optional) + +## Implementierung: + +Füge in `sidebar.ex` hinzu: + +```elixir +defp sidebar_menu(assigns) do + ~H""" + + """ +end + +attr :href, :string, required: true +attr :icon, :string, required: true +attr :label, :string, required: true + +defp menu_item(assigns) do + ~H""" +
  • + <.link + navigate={@href} + class="flex items-center gap-3 tooltip tooltip-right" + data-tip={@label} + role="menuitem" + > + <.icon name={@icon} class="size-5 shrink-0" aria-hidden="true" /> + {@label} + +
  • + """ +end +``` + +Ersetze in `sidebar_content`: +```heex + +<%= if @current_user do %> + <.sidebar_menu /> +<% end %> +``` + +## Testen: +1. Kompiliere und starte Server +2. Prüfe Expanded State: + - Icons + Labels sichtbar + - Hover funktioniert +3. Toggle zu Collapsed: + - Nur Icons sichtbar + - Tooltips erscheinen bei Hover + - Tooltips zeigen richtigen Text +4. Prüfe: + - Einheitlicher Hover-Effekt + - Navigation funktioniert (Links klickbar) +``` + +### Acceptance Criteria: +- ✅ menu_item Komponente erstellt +- ✅ 4 Menü-Items implementiert +- ✅ Tooltips funktionieren (nur collapsed) +- ✅ Icons sichtbar in beiden States +- ✅ Hover-Effekt einheitlich +- ✅ Navigation funktioniert + +--- + +## Task 8: Sidebar Navigation - Nested Menu + +**Agent:** Sonnet 4.5 (komplexer) +**Geschätzte Dauer:** 30 Minuten + +### Prompt: + +``` +Implementiere das verschachtelte "Beiträge"-Menü (Contributions) mit ZWEI verschiedenen Darstellungen je nach State. + +## Problem: +Das Nested Menu muss sich anders verhalten als flat items: +- **Expanded:** Nutzt
    mit für auf/zuklappbar +- **Collapsed:** Nutzt DaisyUI dropdown für Flyout rechts vom Icon + +## Anforderungen: + +1. **Nur EIN Hover-Effekt** (nicht doppelt) +2. **Flyout erscheint rechts** vom Icon im collapsed state +3. **Smooth transitions** zwischen states +4. **Submenu-Items:** Beitragsarten, Einstellungen + +## Implementierung: + +Füge in `sidebar.ex` hinzu: + +```elixir +attr :icon, :string, required: true +attr :label, :string, required: true +slot :inner_block, required: true + +defp menu_group(assigns) do + ~H""" + + """ +end + +attr :href, :string, required: true +attr :label, :string, required: true + +defp menu_subitem(assigns) do + ~H""" +
  • + <.link navigate={@href} role="menuitem"> + {@label} + +
  • + """ +end +``` + +Füge in `sidebar_menu` nach den flat items hinzu: + +```elixir + +<.menu_group + icon="hero-currency-dollar" + label={gettext("Contributions")} +> + <.menu_subitem href="/contribution_types" label={gettext("Contribution Types")} /> + <.menu_subitem href="/contribution_settings" label={gettext("Settings")} /> + +``` + +## CSS-Prüfung: + +Stelle sicher, dass in `app.css` folgende Regeln existieren: + +```css +/* Expanded: Show details, hide dropdown */ +.expanded-menu-group { + @apply block; +} +.collapsed-menu-group { + @apply hidden; +} + +/* Collapsed: Hide details, show dropdown */ +[data-sidebar-expanded="false"] .sidebar .expanded-menu-group { + @apply hidden; +} +[data-sidebar-expanded="false"] .sidebar .collapsed-menu-group { + @apply block; +} +``` + +## Testen: + +1. **Expanded State:** + - Details/Summary erscheint + - Klick öffnet/schließt Submenu + - Submenu-Items sind klickbar + - NUR EIN Hover-Effekt auf summary + +2. **Collapsed State:** + - Dropdown erscheint + - Flyout öffnet RECHTS vom Icon + - Menu-Title "Contributions" erscheint + - Submenu-Items sind klickbar + - NUR EIN Hover-Effekt auf button + +3. **Toggle zwischen States:** + - Smooth Transition + - Keine Glitches + - Keine doppelten Elemente sichtbar + +## Debugging: + +Falls Probleme auftreten: +- Browser DevTools: Prüfe, welche Elemente .expanded-menu-group oder .collapsed-menu-group haben +- Prüfe data-sidebar-expanded Attribut im HTML +- Prüfe z-index des Dropdowns (z-50) +- Prüfe, ob dropdown-right funktioniert +``` + +### Acceptance Criteria: +- ✅ menu_group und menu_subitem implementiert +- ✅ Details funktioniert (expanded) +- ✅ Dropdown funktioniert (collapsed) +- ✅ Flyout erscheint rechts +- ✅ Nur EIN Hover-Effekt +- ✅ Keine visuellen Glitches + +--- + +## Task 9: Sidebar Footer mit Flexbox + +**Agent:** Auto-Mode +**Geschätzte Dauer:** 25 Minuten + +### Prompt: + +``` +Implementiere den Sidebar-Footer mit korrekter Positionierung am unteren Ende. + +## Flexbox-Struktur: + +Die Sidebar muss als Flexbox-Container funktionieren: +1. `.sidebar`: `flex flex-col` (bereits vorhanden) +2. Navigation: `flex-1` (nimmt verfügbaren Platz) +3. Footer: `mt-auto` (wird nach unten geschoben) + +## Footer-Komponenten: + +1. **Language Selector:** + - Nur im expanded state sichtbar (`.expanded-only`) + - DaisyUI select + - Form mit POST zu `/locale` + +2. **Theme Toggle:** + - IMMER sichtbar + - Horizontal: Sun Icon + Toggle + Moon Icon + - DaisyUI toggle + theme-controller + +3. **User Menu:** + - DaisyUI dropdown + - `dropdown-top dropdown-end` (öffnet nach oben) + - Avatar: Erste Buchstabe, zentriert, rund + - Email nur expanded + - Dropdown: Profile + Logout + +## Implementierung: + +```elixir +defp sidebar_footer(assigns) do + ~H""" +
    + +
    + + +
    + + + <.theme_toggle /> + + + <.user_menu current_user={@current_user} /> +
    + """ +end + +defp theme_toggle(assigns) do + ~H""" + + """ +end + +defp user_menu(assigns) do + ~H""" + + """ +end +``` + +Ersetze in `sidebar_content` den Footer-Placeholder: + +```heex + +<%= if @current_user do %> + <.sidebar_footer current_user={@current_user} /> +<% end %> +``` + +Prüfe, dass `.sidebar_menu` `flex-1` hat: + +```heex +