From cc6d72b6b19ff8bed99f0733d8458a298d5a266d Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 13 Jan 2026 11:44:40 +0100 Subject: [PATCH 01/14] feat: add service skeleton and tests --- lib/mv/membership/import/member_csv.ex | 158 ++++++++++++++++++ test/mv/membership/import/member_csv_test.exs | 128 ++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 lib/mv/membership/import/member_csv.ex create mode 100644 test/mv/membership/import/member_csv_test.exs diff --git a/lib/mv/membership/import/member_csv.ex b/lib/mv/membership/import/member_csv.ex new file mode 100644 index 0000000..6e4e019 --- /dev/null +++ b/lib/mv/membership/import/member_csv.ex @@ -0,0 +1,158 @@ +defmodule Mv.Membership.Import.MemberCSV do + @moduledoc """ + Service module for importing members from CSV files. + + This module provides the core API for CSV member import functionality: + - `prepare/2` - Parses and validates CSV content, returns import state + - `process_chunk/3` - Processes a chunk of rows and creates members + + ## Error Handling + + Errors are returned as `%Error{}` structs containing: + - `csv_line_number` - The physical line number in the CSV file + - `field` - The field name (atom) or `nil` if not field-specific + - `message` - Human-readable error message + + ## Import State + + The `import_state` returned by `prepare/2` contains: + - `chunks` - List of row chunks ready for processing + - `column_map` - Map of canonical field names to column indices + - `custom_field_map` - Map of custom field names to column indices + - `warnings` - List of warning messages (e.g., unknown custom field columns) + + ## Chunk Results + + The `chunk_result` returned by `process_chunk/3` contains: + - `inserted` - Number of successfully created members + - `failed` - Number of failed member creations + - `errors` - List of `%Error{}` structs (capped at 50 per import) + + ## Examples + + # Prepare CSV for import + {:ok, import_state} = MemberCSV.prepare(csv_content) + + # Process first chunk + chunk = Enum.at(import_state.chunks, 0) + {:ok, result} = MemberCSV.process_chunk(chunk, import_state.column_map) + """ + + defmodule Error do + @moduledoc """ + Error struct for CSV import errors. + + ## Fields + + - `csv_line_number` - The physical line number in the CSV file (1-based, header is line 1) + - `field` - The field name as an atom (e.g., `:email`) or `nil` if not field-specific + - `message` - Human-readable error message + """ + defstruct csv_line_number: nil, field: nil, message: nil + + @type t :: %__MODULE__{ + csv_line_number: integer(), + field: atom() | nil, + message: String.t() + } + end + + @type import_state :: %{ + chunks: list(list({pos_integer(), map()})), + column_map: %{atom() => non_neg_integer()}, + custom_field_map: %{String.t() => non_neg_integer()}, + warnings: list(String.t()) + } + + @type chunk_result :: %{ + inserted: non_neg_integer(), + failed: non_neg_integer(), + errors: list(Error.t()) + } + + @doc """ + Prepares CSV content for import by parsing, mapping headers, and validating limits. + + This function: + 1. Strips UTF-8 BOM if present + 2. Detects CSV delimiter (semicolon or comma) + 3. Parses headers and data rows + 4. Maps headers to canonical member fields + 5. Maps custom field columns by name + 6. Validates row count limits + 7. Chunks rows for processing + + ## Parameters + + - `file_content` - The raw CSV file content as a string + - `opts` - Optional keyword list: + - `:max_rows` - Maximum number of data rows allowed (default: 1000) + - `:chunk_size` - Number of rows per chunk (default: 200) + + ## Returns + + - `{:ok, import_state}` - Successfully prepared import state + - `{:error, reason}` - Error reason (string or error struct) + + ## Examples + + iex> MemberCSV.prepare("email\\njohn@example.com") + {:ok, %{chunks: [...], column_map: %{email: 0}, ...}} + + iex> MemberCSV.prepare("") + {:error, "CSV file is empty"} + """ + @spec prepare(String.t(), keyword()) :: {:ok, import_state()} | {:error, String.t()} + def prepare(file_content, opts \\ []) do + # TODO: Implement in Issue #3 (CSV Parsing) + # This is a skeleton implementation that will be filled in later + _ = {file_content, opts} + + # Placeholder return - will be replaced with actual implementation + {:error, "Not yet implemented"} + end + + @doc """ + Processes a chunk of CSV rows and creates members. + + This function: + 1. Validates each row + 2. Creates members via Ash resource + 3. Creates custom field values for each member + 4. Collects errors with correct CSV line numbers + 5. Returns chunk processing results + + ## Parameters + + - `chunk_rows_with_lines` - List of tuples `{csv_line_number, row_map}` where: + - `csv_line_number` - Physical line number in CSV (1-based) + - `row_map` - Map of column names to values + - `column_map` - Map of canonical field names (atoms) to column indices + - `opts` - Optional keyword list for processing options + + ## Returns + + - `{:ok, chunk_result}` - Chunk processing results + - `{:error, reason}` - Error reason (string) + + ## Examples + + iex> chunk = [{2, %{"email" => "john@example.com"}}] + iex> column_map = %{email: 0} + iex> MemberCSV.process_chunk(chunk, column_map) + {:ok, %{inserted: 1, failed: 0, errors: []}} + """ + @spec process_chunk( + list({pos_integer(), map()}), + %{atom() => non_neg_integer()}, + keyword() + ) :: {:ok, chunk_result()} | {:error, String.t()} + def process_chunk(chunk_rows_with_lines, column_map, opts \\ []) do + # TODO: Implement in Issue #6 (Persistence) + # This is a skeleton implementation that will be filled in later + _ = {chunk_rows_with_lines, column_map, opts} + + # Placeholder return - will be replaced with actual implementation + {:ok, %{inserted: 0, failed: 0, errors: []}} + end +end diff --git a/test/mv/membership/import/member_csv_test.exs b/test/mv/membership/import/member_csv_test.exs new file mode 100644 index 0000000..1e51d51 --- /dev/null +++ b/test/mv/membership/import/member_csv_test.exs @@ -0,0 +1,128 @@ +defmodule Mv.Membership.Import.MemberCSVTest do + use Mv.DataCase, async: false + + alias Mv.Membership.Import.MemberCSV + + describe "Error struct" do + test "Error struct exists with required fields" do + # This will fail at runtime if the struct doesn't exist + # We use struct/2 to create the struct at runtime + error = + struct(MemberCSV.Error, %{ + csv_line_number: 5, + field: :email, + message: "is not a valid email" + }) + + assert error.csv_line_number == 5 + assert error.field == :email + assert error.message == "is not a valid email" + end + + test "Error struct allows nil field" do + # This will fail at runtime if the struct doesn't exist + error = + struct(MemberCSV.Error, %{ + csv_line_number: 10, + field: nil, + message: "Row is empty" + }) + + assert error.csv_line_number == 10 + assert error.field == nil + assert error.message == "Row is empty" + end + end + + describe "prepare/2" do + test "function exists and accepts file_content and opts" do + file_content = "email\njohn@example.com" + opts = [] + + # This will fail until the function is implemented + result = MemberCSV.prepare(file_content, opts) + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + + test "returns {:ok, import_state} on success" do + file_content = "email\njohn@example.com" + opts = [] + + assert {:ok, import_state} = MemberCSV.prepare(file_content, opts) + + # Check that import_state contains expected fields + assert Map.has_key?(import_state, :chunks) + assert Map.has_key?(import_state, :column_map) + assert Map.has_key?(import_state, :custom_field_map) + assert Map.has_key?(import_state, :warnings) + end + + test "returns {:error, reason} on failure" do + file_content = "" + opts = [] + + assert {:error, _reason} = MemberCSV.prepare(file_content, opts) + end + + test "function has documentation" do + # Check that @doc exists by reading the module + assert function_exported?(MemberCSV, :prepare, 2) + end + end + + describe "process_chunk/3" do + test "function exists and accepts chunk_rows_with_lines, column_map, and opts" do + chunk_rows_with_lines = [{2, %{"email" => "john@example.com"}}] + column_map = %{email: 0} + opts = [] + + # This will fail until the function is implemented + result = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, opts) + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + + test "returns {:ok, chunk_result} on success" do + chunk_rows_with_lines = [{2, %{"email" => "john@example.com"}}] + column_map = %{email: 0} + opts = [] + + assert {:ok, chunk_result} = + MemberCSV.process_chunk(chunk_rows_with_lines, column_map, opts) + + # Check that chunk_result contains expected fields + assert Map.has_key?(chunk_result, :inserted) + assert Map.has_key?(chunk_result, :failed) + assert Map.has_key?(chunk_result, :errors) + assert is_integer(chunk_result.inserted) + assert is_integer(chunk_result.failed) + assert is_list(chunk_result.errors) + end + + test "returns {:error, reason} on failure" do + chunk_rows_with_lines = [] + column_map = %{} + opts = [] + + # This might return {:ok, _} with zero counts or {:error, _} + result = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, opts) + assert match?({:ok, _}, result) or match?({:error, _}, result) + end + + test "function has documentation" do + # Check that @doc exists by reading the module + assert function_exported?(MemberCSV, :process_chunk, 3) + end + end + + describe "module documentation" do + test "module has @moduledoc" do + # Check that the module exists and has documentation + assert Code.ensure_loaded?(MemberCSV) + + # Try to get the module documentation + {:docs_v1, _, _, _, %{"en" => moduledoc}, _, _} = Code.fetch_docs(MemberCSV) + assert is_binary(moduledoc) + assert String.length(moduledoc) > 0 + end + end +end From 55401eda3a92108b6cb5e50be8276065d51e10d2 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 13 Jan 2026 17:20:15 +0100 Subject: [PATCH 02/14] chore: update docs --- CHANGELOG.md | 35 +++ CODE_GUIDELINES.md | 89 +++++-- README.md | 12 +- docs/csv-member-import-v1.md | 3 +- docs/database-schema-readme.md | 72 +++++- docs/database_schema.dbml | 140 ++++++++++- docs/development-progress-log.md | 158 +++++++++++- docs/documentation-sync-todos.md | 128 ++++++++++ docs/feature-roadmap.md | 178 +++++++------ docs/membership-fee-architecture.md | 4 +- docs/membership-fee-overview.md | 4 +- docs/roles-and-permissions-architecture.md | 8 +- ...les-and-permissions-implementation-plan.md | 3 +- docs/roles-and-permissions-overview.md | 4 +- docs/sidebar-analysis-current-state.md | 9 +- docs/sidebar-requirements-v2.md | 3 +- docs/test-failures-analysis.md | 233 ------------------ docs/test-status-membership-fee-ui.md | 137 ---------- docs/umsetzung-sidebar.md | 11 +- 19 files changed, 732 insertions(+), 499 deletions(-) create mode 100644 docs/documentation-sync-todos.md delete mode 100644 docs/test-failures-analysis.md delete mode 100644 docs/test-status-membership-fee-ui.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 28b4a37..2c23c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,27 @@ 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 @@ -19,8 +40,22 @@ 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 5cc792c..636f3fb 100644 --- a/CODE_GUIDELINES.md +++ b/CODE_GUIDELINES.md @@ -83,7 +83,18 @@ 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,7 +118,7 @@ lib/ │ │ ├── table_components.ex │ │ ├── layouts.ex │ │ └── layouts/ # Layout templates -│ │ ├── navbar.ex +│ │ ├── sidebar.ex │ │ └── root.html.heex │ ├── controllers/ # HTTP controllers │ │ ├── auth_controller.ex @@ -123,7 +134,12 @@ lib/ │ │ ├── member_live/ # Member CRUD LiveViews │ │ ├── custom_field_value_live/ # CustomFieldValue CRUD LiveViews │ │ ├── custom_field_live/ -│ │ └── user_live/ # User management LiveViews +│ │ ├── 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) │ ├── auth_overrides.ex # AshAuthentication overrides │ ├── endpoint.ex # Phoenix endpoint │ ├── gettext.ex # I18n configuration @@ -818,14 +834,17 @@ 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/umsetzung-sidebar.md b/docs/umsetzung-sidebar.md deleted file mode 100644 index b77093c..0000000 --- a/docs/umsetzung-sidebar.md +++ /dev/null @@ -1,1579 +0,0 @@ -# Sidebar Neuimplementierung - Schritt-für-Schritt Anleitung - -**Erstellt:** 2025-12-16 -**Last Updated:** 2026-01-13 -**Status:** ⚠️ Veraltet - Sidebar wurde bereits implementiert (2026-01-12, PR #260) -**Strategie:** Sequenzielle Tasks mit frischem Kontext pro Task - -> **Hinweis:** Diese Implementierungs-Anleitung wurde durch die tatsächliche Implementierung obsolet. Die Sidebar wurde erfolgreich implementiert. Siehe `sidebar-requirements-v2.md` für die finale Spezifikation. - ---- - -## Übersicht - -~~Diese Anleitung zerlegt die komplexe Sidebar-Implementierung in 13 beherrschbare Subtasks. Jeder Task wird mit einem frischen Cursor-Agent im Auto-Mode (oder Sonnet 4.5 für komplexere Aufgaben) umgesetzt.~~ - -**Status:** Diese Implementierungs-Anleitung wurde durch die tatsächliche Implementierung obsolet. Die Sidebar ist jetzt vollständig implementiert und funktionsfähig. - ---- - -## Task 1: Vorbereitung & Analyse - -**Agent:** Sonnet 4.5 -**Geschätzte Dauer:** 10 Minuten - -### Prompt: - -``` -Analysiere die aktuelle Sidebar-Implementierung in diesem Projekt und erstelle einen detaillierten Bericht: - -1. Liste alle Dateien auf, die mit der Sidebar zusammenhängen -2. Dokumentiere die aktuelle Struktur (HTML, CSS, JavaScript) -3. Identifiziere alle Custom-CSS-Klassen und Variants -4. Dokumentiere die JavaScript-Hooks und ihre Funktionen -5. Erstelle eine Liste aller Dependencies (DaisyUI-Komponenten, Tailwind-Klassen) - -Speichere den Bericht als `docs/sidebar-analysis-current-state.md` - -Ziel: Vollständiges Verständnis des Ist-Zustands vor der Neuimplementierung. -``` - -### Acceptance Criteria: -- ✅ Alle Sidebar-bezogenen Dateien identifiziert -- ✅ Aktuelle Implementierung dokumentiert -- ✅ Custom CSS und JavaScript dokumentiert -- ✅ Bericht als Markdown gespeichert - ---- - -## Task 2: DaisyUI Drawer Pattern Recherche - -**Agent:** Sonnet 4.5 (wegen Web-Recherche) -**Geschätzte Dauer:** 15 Minuten - -### Prompt: - -``` -Recherchiere und dokumentiere das Standard DaisyUI Drawer Pattern: - -1. Lies die DaisyUI Dokumentation für die `drawer` Komponente -2. Finde Best-Practice-Beispiele für responsive Sidebars mit DaisyUI -3. Dokumentiere, wie drawer-toggle funktioniert -4. Dokumentiere, wie drawer-open für Desktop funktioniert -5. Erstelle Code-Beispiele für: - - Mobile Drawer (overlay) - - Desktop Sidebar (persistent) - - Kombination beider - -Speichere die Dokumentation als `docs/daisyui-drawer-pattern.md` - -Wichtig: Verwende KEINE custom CSS variants - nur Standard DaisyUI und Tailwind. -``` - -### Acceptance Criteria: -- ✅ DaisyUI Drawer Pattern dokumentiert -- ✅ Mobile und Desktop Patterns verstanden -- ✅ Code-Beispiele vorhanden -- ✅ Dokumentation gespeichert - ---- - -## Task 3: Anforderungsdefinition & Design - -**Agent:** Sonnet 4.5 -**Geschätzte Dauer:** 20 Minuten - -### Prompt: - -``` -Erstelle eine präzise Anforderungsspezifikation für die Sidebar basierend auf folgenden Anforderungen: - -## Funktionale Anforderungen: - -1. **Logo:** - - Immer sichtbar, gleiche Größe (32px / size-8) - - Sowohl im expanded als auch collapsed State - - Keine zwei verschiedenen Logo-Elemente - -2. **Toggle-Button:** - - Nur auf Desktop sichtbar - - Icon-Swap: Chevron-left (expanded) ↔ Chevron-right (collapsed) - - Immer erreichbar - -3. **Menü-Items:** - - Expanded: Icons + Text-Labels - - Collapsed: Nur Icons mit Tooltips (tooltip-right) - - Einheitlicher Hover-Effekt - -4. **Nested Menu "Beiträge":** - - Expanded: Standard
mit - - Collapsed: DaisyUI dropdown dropdown-right - - Nur EIN Hover-Effekt, kein doppelter - -5. **Footer:** - - IMMER am unteren Ende der Sidebar (via Flexbox) - - Theme-Toggle (immer sichtbar) - - Language-Selector (nur expanded) - - User-Menu mit Avatar (dropdown-top dropdown-end) - - Avatar: Erste Buchstabe, zentriert - -6. **State Persistence:** - - localStorage: 'sidebar-expanded' - - data-attribute: [data-sidebar-expanded="true"|"false"] - -7. **Responsive:** - - Mobile: Standard DaisyUI Drawer Overlay - - Desktop: Fixed Sidebar mit smooth width transition - -## Aufgaben: - -1. Erstelle Wireframes (als ASCII-Art) für: - - Desktop Expanded - - Desktop Collapsed - - Mobile mit Overlay - -2. Liste alle benötigten DaisyUI-Komponenten auf - -3. Definiere CSS-Strategie: - - Nur Tailwind + DaisyUI - - KEINE custom variants (@custom-variant) - - State-Management via data-attribute selectors - -4. Definiere State-Management-Strategie: - - JavaScript Hook - - localStorage - - CSS reactions - -Speichere als `docs/sidebar-requirements-v2.md` -``` - -### Acceptance Criteria: -- ✅ Anforderungen klar dokumentiert -- ✅ Wireframes erstellt -- ✅ Komponenten-Liste vorhanden -- ✅ CSS-Strategie definiert -- ✅ State-Management definiert - ---- - -## Task 4: CSS Foundation - -**Agent:** Auto-Mode -**Geschätzte Dauer:** 15 Minuten - -### Prompt: - -``` -Erstelle die CSS-Grundlage für die neue Sidebar in `assets/css/app.css`: - -## Schritt 1: Aufräumen -1. Entferne ALLE bestehenden Custom CSS Variants für Sidebar: - - @custom-variant is-drawer-open - - @custom-variant is-drawer-close - - Alle .is-drawer-* Regeln - -2. Entferne alte Sidebar-spezifische Custom-Klassen - -## Schritt 2: Neue CSS-Regeln erstellen - -Erstelle CSS basierend auf `[data-sidebar-expanded]` Attribut: - -```css -/* Desktop Sidebar Base */ -.sidebar { - @apply flex flex-col bg-base-200 min-h-screen; - @apply transition-[width] duration-300 ease-in-out; - width: 16rem; /* Expanded: w-64 */ -} - -/* Collapsed State */ -[data-sidebar-expanded="false"] .sidebar { - width: 4rem; /* Collapsed: w-16 */ -} - -/* Text Labels - Hide in Collapsed State */ -.menu-label { - @apply transition-all duration-200 whitespace-nowrap; -} - -[data-sidebar-expanded="false"] .sidebar .menu-label { - @apply opacity-0 w-0 overflow-hidden pointer-events-none; -} - -/* Toggle Button Icon Swap */ -.sidebar-collapsed-icon { - @apply hidden; -} - -[data-sidebar-expanded="false"] .sidebar-expanded-icon { - @apply hidden; -} - -[data-sidebar-expanded="false"] .sidebar-collapsed-icon { - @apply block; -} - -/* Menu Groups - Show/Hide Based on State */ -.expanded-menu-group { - @apply block; -} - -.collapsed-menu-group { - @apply hidden; -} - -[data-sidebar-expanded="false"] .sidebar .expanded-menu-group { - @apply hidden; -} - -[data-sidebar-expanded="false"] .sidebar .collapsed-menu-group { - @apply block; -} - -/* Elements Only Visible in Expanded State */ -.expanded-only { - @apply block transition-opacity duration-200; -} - -[data-sidebar-expanded="false"] .sidebar .expanded-only { - @apply hidden; -} - -/* Tooltip - Only Show in Collapsed State */ -.sidebar .tooltip::before, -.sidebar .tooltip::after { - @apply opacity-0 pointer-events-none; -} - -[data-sidebar-expanded="false"] .sidebar .tooltip:hover::before, -[data-sidebar-expanded="false"] .sidebar .tooltip:hover::after { - @apply opacity-100; -} - -/* Menu Item Alignment */ -[data-sidebar-expanded="false"] .sidebar .menu > li > a, -[data-sidebar-expanded="false"] .sidebar .menu > li > button { - @apply justify-center px-0; -} -``` - -## Schritt 3: Testen -- Kompiliere CSS: `mix assets.build` -- Prüfe auf Fehler -- Stelle sicher, dass keine alten Custom-Variants mehr existieren - -Verwende AUSSCHLIESSLICH: -- Tailwind @apply -- Standard DaisyUI Klassen -- CSS attribute selectors für state -``` - -### Acceptance Criteria: -- ✅ Alte Custom Variants entfernt -- ✅ Neue CSS-Regeln erstellt -- ✅ CSS kompiliert ohne Fehler -- ✅ Nur Standard Tailwind/DaisyUI verwendet - ---- - -## Task 5: Layout-Struktur - -**Agent:** Auto-Mode -**Geschätzte Dauer:** 20 Minuten - -### Prompt: - -``` -Implementiere die grundlegende Layout-Struktur für die Sidebar in `lib/mv_web/components/layouts.ex`: - -## Anforderungen: - -1. Nutze DaisyUI `drawer` + `drawer-open` Pattern -2. Ein Container für Mobile UND Desktop (keine Duplikate!) -3. `@inner_block` darf nur EINMAL gerendert werden -4. `data-sidebar-expanded` Attribut auf root -5. `phx-hook="SidebarState"` auf root -6. `id="main-sidebar"` auf main content (für Tests) - -## Implementierung: - -Ersetze die bestehende `app/1` Funktion mit: - -```heex -def app(assigns) do - club_name = get_club_name() - assigns = assign(assigns, :club_name, club_name) - - ~H""" - <%= if @current_user do %> -
- - -
- - - - -
-
- {render_slot(@inner_block)} -
-
-
- -
- - - -
-
- <% else %> - -
-
- {render_slot(@inner_block)} -
-
- <% end %> - - <.flash_group flash={@flash} /> - """ -end -``` - -## Wichtig: -- @inner_block wird nur EINMAL gerendert (im drawer-content) -- Mobile und Desktop teilen sich den gleichen main-content -- Sidebar-Inhalt kommt in späteren Tasks - -## Testen: -1. Kompiliere: `mix compile` -2. Starte Server: `mix phx.server` -3. Prüfe: Keine duplicate ID Fehler in Browser Console -4. Prüfe: Layout funktioniert responsive -5. Prüfe: Mobile Header erscheint nur auf Mobile -``` - -### Acceptance Criteria: -- ✅ Layout-Struktur implementiert -- ✅ Keine duplicate IDs -- ✅ @inner_block nur einmal gerendert -- ✅ Responsive funktioniert -- ✅ Kompiliert ohne Fehler - ---- - -## Task 6: Sidebar Header Komponente - -**Agent:** Auto-Mode -**Geschätzte Dauer:** 20 Minuten - -### Prompt: - -``` -Implementiere die Sidebar-Header-Komponente in `lib/mv_web/components/layouts/sidebar.ex`: - -## Anforderungen: - -1. **Logo:** - - Immer `size-8` (32px) - - Immer sichtbar (kein Hide) - - Nur EIN Logo-Element - - Pfad: `/images/mila.svg` - -2. **Club-Name:** - - Text-Label mit CSS-Klasse `menu-label` - - Wird via CSS ausgeblendet (collapsed) - - `text-lg font-bold truncate` - -3. **Toggle-Button:** - - Nur auf Desktop sichtbar (responsive: `hidden lg:flex` oder `lg:block`) - - DaisyUI-konforme Button-Variante wählen: - * Option A: `btn btn-ghost btn-sm btn-square` (minimal, icon-only) - * Option B: `btn btn-ghost btn-sm` (mit etwas Padding) - * Option C: `btn btn-ghost btn-circle` (rund, falls besser zum Design passt) - - Icon-Strategie (wähle die beste Variante): - * Option A: Zwei Icons mit CSS-Klassen (`.sidebar-expanded-icon` / `.sidebar-collapsed-icon`) - * Option B: Ein Icon mit CSS transform (rotate bei collapsed) - * Option C: Ein Icon mit CSS content-swap (via ::before/::after) - - Event-Handler (wähle passende Variante): - * Option A: `onclick="toggleSidebar()"` (wenn global function vorhanden) - * Option B: `phx-click="toggle_sidebar"` (wenn LiveView event) - * Option C: `phx-hook="SidebarToggle"` (wenn Hook-basiert) - - ARIA: `aria-label={gettext("Toggle sidebar")}` und `aria-expanded` (wird via JS gesetzt) - -## Design-Überlegungen: - -- Button sollte sich harmonisch in den Header einfügen -- Position: Rechts im Header (`ml-auto`) -- Icon-Größe: `size-5` oder `size-4` (je nach Button-Größe) -- Icon-Typ: Chevron (left/right) oder Arrow (left/right) - wähle das passendere -- Hover-Effekt: Standard DaisyUI btn-ghost hover - -## Implementierung: - -Erstelle `sidebar_header/1` Funktion mit: -- Flexbox-Layout für Header (Logo + Name + Toggle) -- Responsive Toggle-Button -- Icon-Swap-Mechanismus (wähle beste Variante) -- Korrekte ARIA-Attribute - -## Empfehlung: - -Für DaisyUI-Konformität und Wartbarkeit: -- Button: `btn btn-ghost btn-sm btn-square` (icon-only, minimal) -- Icons: Zwei separate Icons mit CSS-Klassen (einfach, klar) -- Event: `onclick="toggleSidebar()"` (wenn JS Hook vorhanden) ODER `phx-click` (wenn LiveView) - -## Beispiel-Struktur (als Orientierung): - -```elixir -defp sidebar_header(assigns) do - ~H""" -
- - Mila Logo - - - - {@club_name} - - - - <%= unless @mobile do %> - - <% 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 -