diff --git a/Justfile b/Justfile
index 876591d..25fb35c 100644
--- a/Justfile
+++ b/Justfile
@@ -90,7 +90,7 @@ clean:
remove-gettext-conflicts:
#!/usr/bin/env bash
set -euo pipefail
- find priv/gettext -type f -exec sed -i '/^<<<<<<< HEAD$/d; /^=======$/d; /^>>>>>>>/d' {} \;
+ find priv/gettext -type f -exec sed -i '/^<<<<<<>>>>>>/d; /^%%%%%%%/d; /^++++++/d; s/^+//' {} \;
# Production environment commands
# ================================
diff --git a/config/test.exs b/config/test.exs
index bcb55eb..2c4d2ba 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -45,3 +45,6 @@ config :mv, :token_signing_secret, "test_secret_key_for_ash_authentication_token
config :mv, :session_identifier, :unsafe
config :mv, :require_token_presence_for_authentication, false
+
+# Enable SQL Sandbox for async LiveView tests
+config :mv, :sql_sandbox, true
diff --git a/docs/contributions-architecture.md b/docs/contributions-architecture.md
new file mode 100644
index 0000000..3718a3b
--- /dev/null
+++ b/docs/contributions-architecture.md
@@ -0,0 +1,653 @@
+# Membership Contributions - Technical Architecture
+
+**Project:** Mila - Membership Management System
+**Feature:** Membership Contribution Management
+**Version:** 1.0
+**Last Updated:** 2025-11-27
+**Status:** Architecture Design - Ready for Implementation
+
+---
+
+## Purpose
+
+This document defines the technical architecture for the Membership Contributions system. It focuses on architectural decisions, patterns, module structure, and integration points **without** concrete implementation details.
+
+**Related Documents:**
+- [contributions-overview.md](./contributions-overview.md) - Business logic and requirements
+- [database-schema-readme.md](./database-schema-readme.md) - Database documentation
+- [database_schema.dbml](./database_schema.dbml) - Database schema definition
+
+---
+
+## Table of Contents
+
+1. [Architecture Principles](#architecture-principles)
+2. [Domain Structure](#domain-structure)
+3. [Data Architecture](#data-architecture)
+4. [Business Logic Architecture](#business-logic-architecture)
+5. [Integration Points](#integration-points)
+6. [Acceptance Criteria](#acceptance-criteria)
+7. [Testing Strategy](#testing-strategy)
+8. [Security Considerations](#security-considerations)
+9. [Performance Considerations](#performance-considerations)
+
+---
+
+## Architecture Principles
+
+### Core Design Decisions
+
+1. **Single Responsibility:**
+ - Each module has one clear responsibility
+ - Period generation separated from status management
+ - Calendar logic isolated in dedicated module
+
+2. **No Redundancy:**
+ - No `period_end` field (calculated from `period_start` + `interval`)
+ - No `interval_type` field (read from `contribution_type.interval`)
+ - Eliminates data inconsistencies
+
+3. **Immutability Where Important:**
+ - `contribution_type.interval` cannot be changed after creation
+ - Prevents complex migration scenarios
+ - Enforced via Ash change validation
+
+4. **Historical Accuracy:**
+ - `amount` stored per period for audit trail
+ - Enables tracking of contribution changes over time
+ - Old periods retain original amounts
+
+5. **Calendar-Based Periods:**
+ - All periods aligned to calendar boundaries
+ - Simplifies date calculations
+ - Predictable period generation
+
+---
+
+## Domain Structure
+
+### Ash Domain: `Mv.Contributions`
+
+**Purpose:** Encapsulates all contribution-related resources and logic
+
+**Resources:**
+- `ContributionType` - Contribution type definitions (admin-managed)
+- `ContributionPeriod` - Individual contribution periods per member
+
+**Extensions:**
+- Member resource extended with contribution fields
+
+### Module Organization
+
+```
+lib/
+├── contributions/
+│ ├── contributions.ex # Ash domain definition
+│ ├── contribution_type.ex # ContributionType resource
+│ ├── contribution_period.ex # ContributionPeriod resource
+│ └── changes/
+│ ├── prevent_interval_change.ex # Validates interval immutability
+│ ├── set_contribution_start_date.ex # Auto-sets start date
+│ └── validate_same_interval.ex # Validates interval match on type change
+├── mv/
+│ └── contributions/
+│ ├── period_generator.ex # Period generation algorithm
+│ └── calendar_periods.ex # Calendar period calculations
+└── membership/
+ └── member.ex # Extended with contribution relationships
+```
+
+### Separation of Concerns
+
+**Domain Layer (Ash Resources):**
+- Data validation
+- Relationship management
+- Policy enforcement
+- Action definitions
+
+**Business Logic Layer (`Mv.Contributions`):**
+- Period generation algorithm
+- Calendar calculations
+- Date boundary handling
+- Status transitions
+
+**UI Layer (LiveView):**
+- User interaction
+- Display logic
+- Authorization checks
+- Form handling
+
+---
+
+## Data Architecture
+
+### Database Schema Extensions
+
+**See:** [database-schema-readme.md](./database-schema-readme.md) and [database_schema.dbml](./database_schema.dbml) for complete schema documentation.
+
+### New Tables
+
+1. **`contribution_types`**
+ - Purpose: Define contribution types with fixed intervals
+ - Key Constraint: `interval` field immutable after creation
+ - Relationships: has_many members, has_many contribution_periods
+
+2. **`contribution_periods`**
+ - Purpose: Individual contribution periods for members
+ - Key Design: NO `period_end` or `interval_type` fields (calculated)
+ - Relationships: belongs_to member, belongs_to contribution_type
+ - Composite uniqueness: One period per member per period_start
+
+### Member Table Extensions
+
+**Fields Added:**
+- `contribution_type_id` (FK, NOT NULL with default from settings)
+- `contribution_start_date` (Date, nullable)
+
+**Existing Fields Used:**
+- `joined_at` - For calculating contribution start
+- `left_at` - For limiting period generation
+- These fields must remain member fields and should not be replaced by custom fields in the future
+
+### Settings Integration
+
+**Global Settings:**
+- `contributions.include_joining_period` (Boolean)
+- `contributions.default_contribution_type_id` (UUID)
+
+**Storage:** Existing settings mechanism (TBD: dedicated table or configuration resource)
+
+### Foreign Key Behaviors
+
+| Relationship | On Delete | Rationale |
+|--------------|-----------|-----------|
+| `contribution_periods.member_id → members.id` | CASCADE | Remove periods when member deleted |
+| `contribution_periods.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if periods exist |
+| `members.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if assigned to members |
+
+---
+
+## Business Logic Architecture
+
+### Period Generation System
+
+**Component:** `Mv.Contributions.PeriodGenerator`
+
+**Responsibilities:**
+- Calculate which periods should exist for a member
+- Generate missing periods
+- Respect contribution_start_date and left_at boundaries
+- Skip existing periods (idempotent)
+
+**Triggers:**
+1. Member contribution type assigned (via Ash change)
+2. Member created with contribution type (via Ash change)
+3. Scheduled job runs (daily/weekly cron)
+4. Admin manual regeneration (UI action)
+
+**Algorithm Steps:**
+1. Retrieve member with contribution_type and dates
+2. Determine first period start (based on contribution_start_date)
+3. Calculate all period starts from first to today (or left_at)
+4. Query existing periods for member
+5. Generate missing periods with current contribution_type.amount
+6. Insert new periods (batch operation)
+
+**Edge Case Handling:**
+- If contribution_start_date is NULL: Calculate from joined_at + global setting
+- If left_at is set: Stop generation at left_at
+- If contribution_type changes: Handled separately by regeneration logic
+
+### Calendar Period Calculations
+
+**Component:** `Mv.Contributions.CalendarPeriods`
+
+**Responsibilities:**
+- Calculate period boundaries based on interval type
+- Determine current period
+- Determine last completed period
+- Calculate period_end from period_start + interval
+
+**Functions (high-level):**
+- `calculate_period_start/3` - Given date and interval, find period start
+- `calculate_period_end/2` - Given period_start and interval, calculate end
+- `next_period_start/2` - Given period_start and interval, find next
+- `is_current_period?/2` - Check if period contains today
+- `is_last_completed_period?/2` - Check if period just ended
+
+**Interval Logic:**
+- **Monthly:** Start = 1st of month, End = last day of month
+- **Quarterly:** Start = 1st of quarter (Jan/Apr/Jul/Oct), End = last day of quarter
+- **Half-yearly:** Start = 1st of half (Jan/Jul), End = last day of half
+- **Yearly:** Start = Jan 1st, End = Dec 31st
+
+### Status Management
+
+**Component:** Ash actions on `ContributionPeriod`
+
+**Status Transitions:**
+- Simple state machine: unpaid ↔ paid ↔ suspended
+- No complex validation (all transitions allowed)
+- Permissions checked via Ash policies
+
+**Actions Required:**
+- `mark_as_paid` - Set status to :paid
+- `mark_as_suspended` - Set status to :suspended
+- `mark_as_unpaid` - Set status to :unpaid (error correction)
+
+**Bulk Operations:**
+- `bulk_mark_as_paid` - Mark multiple periods as paid (efficiency)
+ - low priority, can be a future issue
+
+### Contribution Type Change Handling
+
+**Component:** Ash change on `Member.contribution_type_id`
+
+**Validation:**
+- Check if new type has same interval as old type
+- If different: Reject change (MVP constraint)
+- If same: Allow change
+
+**Side Effects on Allowed Change:**
+1. Keep all existing periods unchanged
+2. Find future unpaid periods
+3. Delete future unpaid periods
+4. Regenerate periods with new contribution_type_id and amount
+
+**Implementation Pattern:**
+- Use Ash change module to validate
+- Use after_action hook to trigger regeneration
+- Use transaction to ensure atomicity
+
+---
+
+## Integration Points
+
+### Member Resource Integration
+
+**Extension Points:**
+1. Add fields via migration
+2. Add relationships (belongs_to, has_many)
+3. Add calculations (current_period_status, overdue_count)
+4. Add changes (auto-set contribution_start_date, validate interval)
+
+**Backward Compatibility:**
+- New fields nullable or with defaults
+- Existing members get default contribution type from settings
+- No breaking changes to existing member functionality
+
+### Settings System Integration
+
+**Requirements:**
+- Store two global settings
+- Provide UI for admin to modify
+- Default values if not set
+- Validation (e.g., default_contribution_type_id must exist)
+
+**Access Pattern:**
+- Read settings during period generation
+- Read settings during member creation
+- Write settings only via admin UI
+
+### Permission System Integration
+
+**See:** [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md)
+
+**Required Permissions:**
+- `ContributionType.create/update/destroy` - Admin only
+- `ContributionType.read` - Admin, Treasurer, Board
+- `ContributionPeriod.update` (status changes) - Admin, Treasurer
+- `ContributionPeriod.read` - Admin, Treasurer, Board, Own member
+
+**Policy Patterns:**
+- Use existing HasPermission check
+- Leverage existing roles (Admin, Kassenwart)
+- Member can read own periods (linked via member_id)
+
+### LiveView Integration
+
+**New LiveViews Required:**
+1. ContributionType index/form (admin)
+2. ContributionPeriod table component (member detail view)
+3. Settings form section (admin)
+4. Member list column (contribution status)
+
+**Existing LiveViews to Extend:**
+- Member detail view: Add contributions section
+- Member list view: Add status column
+- Settings page: Add contributions section
+
+**Authorization Helpers:**
+- Use existing `can?/3` helper for UI conditionals
+- Check permissions before showing actions
+
+---
+
+## Acceptance Criteria
+
+### ContributionType Resource
+
+**AC-CT-1:** Admin can create contribution type with name, amount, interval, description
+**AC-CT-2:** Interval field is immutable after creation (validation error on change attempt)
+**AC-CT-3:** Admin can update name, amount, description (but not interval)
+**AC-CT-4:** Cannot delete contribution type if assigned to members
+**AC-CT-5:** Cannot delete contribution type if periods exist referencing it
+**AC-CT-6:** Interval must be one of: monthly, quarterly, half_yearly, yearly
+
+### ContributionPeriod Resource
+
+**AC-CP-1:** Period has period_start, status, amount, notes, member_id, contribution_type_id
+**AC-CP-2:** Period_end is calculated, not stored
+**AC-CP-3:** Status defaults to :unpaid
+**AC-CP-4:** One period per member per period_start (uniqueness constraint)
+**AC-CP-5:** Amount is set at generation time from contribution_type.amount
+**AC-CP-6:** Periods cascade delete when member deleted
+**AC-CP-7:** Admin/Treasurer can change status
+**AC-CP-8:** Member can read own periods
+
+### Member Extensions
+
+**AC-M-1:** Member has contribution_type_id field (NOT NULL with default)
+**AC-M-2:** Member has contribution_start_date field (nullable)
+**AC-M-3:** New members get default contribution type from global setting
+**AC-M-4:** contribution_start_date auto-set based on joined_at and global setting
+**AC-M-5:** Admin can manually override contribution_start_date
+**AC-M-6:** Cannot change to contribution type with different interval (MVP)
+
+### Period Generation
+
+**AC-PG-1:** Periods generated when member gets contribution type
+**AC-PG-2:** Periods generated when member created (via change hook)
+**AC-PG-3:** Scheduled job generates missing periods daily
+**AC-PG-4:** Generation respects contribution_start_date
+**AC-PG-5:** Generation stops at left_at if member exited
+**AC-PG-6:** Generation is idempotent (skips existing periods)
+**AC-PG-7:** Periods align to calendar boundaries (1st of month/quarter/half/year)
+**AC-PG-8:** Amount comes from contribution_type at generation time
+
+### Calendar Logic
+
+**AC-CL-1:** Monthly periods: 1st to last day of month
+**AC-CL-2:** Quarterly periods: 1st of Jan/Apr/Jul/Oct to last day of quarter
+**AC-CL-3:** Half-yearly periods: 1st of Jan/Jul to last day of half
+**AC-CL-4:** Yearly periods: Jan 1 to Dec 31
+**AC-CL-5:** Period_end calculated correctly for all interval types
+**AC-CL-6:** Current period determined correctly based on today's date
+**AC-CL-7:** Last completed period determined correctly
+
+### Contribution Type Change
+
+**AC-TC-1:** Can change to type with same interval
+**AC-TC-2:** Cannot change to type with different interval (error message)
+**AC-TC-3:** On allowed change: future unpaid periods regenerated
+**AC-TC-4:** On allowed change: paid/suspended periods unchanged
+**AC-TC-5:** On allowed change: amount updated to new type's amount
+**AC-TC-6:** Change is atomic (transaction)
+
+### Settings
+
+**AC-S-1:** Global setting: include_joining_period (boolean, default true)
+**AC-S-2:** Global setting: default_contribution_type_id (UUID, required)
+**AC-S-3:** Admin can modify settings via UI
+**AC-S-4:** Settings validated (e.g., default type must exist)
+**AC-S-5:** Settings applied to new members immediately
+
+### UI - Member List
+
+**AC-UI-ML-1:** New column shows contribution status
+**AC-UI-ML-2:** Default: Shows last completed period status
+**AC-UI-ML-3:** Optional: Toggle to show current period status
+**AC-UI-ML-4:** Color coding: green (paid), red (unpaid), gray (suspended)
+**AC-UI-ML-5:** Filter: Unpaid in last period
+**AC-UI-ML-6:** Filter: Unpaid in current period
+
+### UI - Member Detail
+
+**AC-UI-MD-1:** Contributions section shows all periods
+**AC-UI-MD-2:** Table columns: Period, Interval, Amount, Status, Actions
+**AC-UI-MD-3:** Checkbox per period for bulk marking (low prio)
+**AC-UI-MD-4:** "Mark selected as paid" button
+**AC-UI-MD-5:** Dropdown to change contribution type (same interval only)
+**AC-UI-MD-6:** Warning if different interval selected
+**AC-UI-MD-7:** Only show actions if user has permission
+
+### UI - Contribution Types Admin
+
+**AC-UI-CTA-1:** List all contribution types
+**AC-UI-CTA-2:** Show: Name, Amount, Interval, Member count
+**AC-UI-CTA-3:** Create new contribution type form
+**AC-UI-CTA-4:** Edit form: Name, Amount, Description editable
+**AC-UI-CTA-5:** Edit form: Interval grayed out (not editable)
+**AC-UI-CTA-6:** Warning on amount change (explain impact)
+**AC-UI-CTA-7:** Cannot delete if members assigned
+**AC-UI-CTA-8:** Only admin can access
+
+### UI - Settings Admin
+
+**AC-UI-SA-1:** Contributions section in settings
+**AC-UI-SA-2:** Dropdown to select default contribution type
+**AC-UI-SA-3:** Checkbox: Include joining period
+**AC-UI-SA-4:** Explanatory text with examples
+**AC-UI-SA-5:** Save button with validation
+
+---
+
+## Testing Strategy
+
+### Unit Testing
+
+**Period Generator Tests:**
+- Correct period_start calculation for all interval types
+- Correct period count from start to end date
+- Respects contribution_start_date boundary
+- Respects left_at boundary
+- Skips existing periods (idempotent)
+- Handles edge dates (year boundaries, leap years)
+
+**Calendar Periods Tests:**
+- Period boundaries correct for all intervals
+- Period_end calculation correct
+- Current period detection
+- Last completed period detection
+- Next period calculation
+
+**Validation Tests:**
+- Interval immutability enforced
+- Same interval validation on type change
+- Status transitions allowed
+- Uniqueness constraints enforced
+
+### Integration Testing
+
+**Period Generation Flow:**
+- Member creation triggers generation
+- Type assignment triggers generation
+- Type change regenerates future periods
+- Scheduled job generates missing periods
+- Left member stops generation
+
+**Status Management Flow:**
+- Mark single period as paid
+- Bulk mark multiple periods (low prio)
+- Status transitions work
+- Permissions enforced
+
+**Contribution Type Management:**
+- Create type
+- Update amount (regeneration triggered)
+- Cannot update interval
+- Cannot delete if in use
+
+### LiveView Testing
+
+**Member List:**
+- Status column displays correctly
+- Toggle between last/current works
+- Filters work correctly
+- Color coding applied
+
+**Member Detail:**
+- Periods table displays all periods
+- Checkboxes work
+- Bulk marking works (low prio)
+- Type change validation works
+- Actions only shown with permission
+
+**Admin UI:**
+- Type CRUD works
+- Settings save correctly
+- Validations display errors
+- Only authorized users can access
+
+### Edge Case Testing
+
+**Interval Change Attempt:**
+- Error message displayed
+- No data modified
+- User can cancel/choose different type
+
+**Exit with Unpaid:**
+- Warning shown
+- Option to suspend offered
+- Exit completes correctly
+
+**Amount Change:**
+- Warning displayed
+- Only future unpaid regenerated
+- Historical periods unchanged
+
+**Date Boundaries:**
+- Today = period start handled
+- Today = period end handled
+- Leap year handled
+
+### Performance Testing
+
+**Period Generation:**
+- Generate 10 years of monthly periods: < 100ms
+- Generate for 1000 members: < 5 seconds
+- Idempotent check efficient (no full scan)
+
+**Member List Query:**
+- With status column: < 200ms for 1000 members
+- Filters applied efficiently
+- No N+1 queries
+
+---
+
+## Security Considerations
+
+### Authorization
+
+**Permissions Required:**
+- ContributionType management: Admin only
+- ContributionPeriod status changes: Admin + Treasurer
+- View all periods: Admin + Treasurer + Board
+- View own periods: All authenticated users
+
+**Policy Enforcement:**
+- All actions protected by Ash policies
+- UI shows/hides based on permissions
+- Backend validates permissions (never trust UI alone)
+
+### Data Integrity
+
+**Validation Layers:**
+1. Database constraints (NOT NULL, UNIQUE, CHECK)
+2. Ash validations (business rules)
+3. UI validations (user experience)
+
+**Immutability Protection:**
+- Interval change prevented at multiple layers
+- Period amounts immutable (audit trail)
+- Settings changes logged (future)
+
+### Audit Trail
+
+**Tracked Information:**
+- Period status changes (who, when) - future enhancement
+- Type amount changes (implicit via period amounts)
+- Member type assignments (via timestamps)
+
+---
+
+## Performance Considerations
+
+### Database Indexes
+
+**Required Indexes:**
+- `contribution_periods(member_id)` - For member period lookups
+- `contribution_periods(contribution_type_id)` - For type queries
+- `contribution_periods(status)` - For unpaid filters
+- `contribution_periods(period_start)` - For date range queries
+- `contribution_periods(member_id, period_start)` - Composite unique index
+- `members(contribution_type_id)` - For type membership count
+
+### Query Optimization
+
+**Preloading:**
+- Load contribution_type with periods (avoid N+1)
+- Load periods when displaying member detail
+- Use Ash's load for efficient preloading
+
+**Calculated Fields:**
+- period_end calculated on-demand (not stored)
+- current_period_status calculated when needed
+- Use Ash calculations for lazy evaluation
+
+**Pagination:**
+- Period list paginated if > 50 periods
+- Member list already paginated
+
+### Caching Strategy
+
+**No caching needed in MVP:**
+- Contribution types rarely change
+- Period queries are fast
+- Settings read infrequently
+
+**Future caching if needed:**
+- Cache settings in application memory
+- Cache contribution types list
+- Invalidate on change
+
+### Scheduled Job Performance
+
+**Period Generation Job:**
+- Run daily or weekly (not hourly)
+- Batch members (process 100 at a time)
+- Skip members with no changes
+- Log failures for retry
+
+---
+
+## Future Enhancements
+
+### Phase 2: Interval Change Support
+
+**Architecture Changes:**
+- Add logic to handle period overlaps
+- Calculate prorata amounts if needed
+- More complex validation
+- Migration path for existing periods
+
+### Phase 3: Payment Details
+
+**Architecture Changes:**
+- Add PaymentTransaction resource
+- Link transactions to periods
+- Support multiple payments per period
+- Reconciliation logic
+
+### Phase 4: vereinfacht.digital Integration
+
+**Architecture Changes:**
+- External API client module
+- Webhook handling for transactions
+- Automatic matching logic
+- Manual review interface
+
+---
+
+**End of Architecture Document**
+
diff --git a/docs/contributions-overview.md b/docs/contributions-overview.md
new file mode 100644
index 0000000..e0c4bc8
--- /dev/null
+++ b/docs/contributions-overview.md
@@ -0,0 +1,527 @@
+# Membership Contributions - Overview
+
+**Project:** Mila - Membership Management System
+**Feature:** Membership Contribution Management
+**Version:** 1.0
+**Last Updated:** 2025-11-27
+**Status:** Concept - Ready for Review
+
+---
+
+## Purpose
+
+This document provides a comprehensive overview of the Membership Contributions system. It covers business logic, data model, UI/UX design, and technical architecture in a concise, bullet-point format.
+
+**For detailed implementation:** See [contributions-implementation-plan.md](./contributions-implementation-plan.md) (created after concept iterations)
+
+---
+
+## Table of Contents
+
+1. [Core Principle](#core-principle)
+2. [Terminology](#terminology)
+3. [Data Model](#data-model)
+4. [Business Logic](#business-logic)
+5. [UI/UX Design](#uiux-design)
+6. [Edge Cases](#edge-cases)
+7. [Technical Integration](#technical-integration)
+8. [Implementation Scope](#implementation-scope)
+
+---
+
+## Core Principle
+
+**Maximum Simplicity:**
+
+- Minimal complexity
+- Clear data model without redundancies
+- Intuitive operation
+- Calendar period-based (Month/Quarter/Half-Year/Year)
+
+---
+
+## Terminology
+
+### German ↔ English
+
+**Core Entities:**
+
+- Beitragsart ↔ Contribution Type / Membership Fee Type
+- Beitragsintervall ↔ Contribution Period
+- Mitgliedsbeitrag ↔ Membership Fee / Contribution
+
+**Status:**
+
+- bezahlt ↔ paid
+- unbezahlt ↔ unpaid
+- ausgesetzt ↔ suspended / waived
+
+**Intervals:**
+
+- monatlich ↔ monthly
+- quartalsweise ↔ quarterly
+- halbjährlich ↔ half-yearly / semi-annually
+- jährlich ↔ yearly / annually
+
+**UI Elements:**
+
+- "Letztes Intervall" ↔ "Last Period" (e.g., 2023 when in 2024)
+- "Aktuelles Intervall" ↔ "Current Period" (e.g., 2024)
+- "Als bezahlt markieren" ↔ "Mark as paid"
+- "Aussetzen" ↔ "Suspend" / "Waive"
+
+---
+
+## Data Model
+
+### Contribution Type (ContributionType)
+
+```
+- id (UUID)
+- name (String) - e.g., "Regular", "Reduced", "Student"
+- amount (Decimal) - Contribution amount in Euro
+- interval (Enum) - :monthly, :quarterly, :half_yearly, :yearly
+- description (Text, optional)
+- timestamps
+```
+
+**Important:**
+
+- `interval` is **IMMUTABLE** after creation!
+- Admin can only change `name`, `amount`, `description`
+- On change: Future unpaid periods regenerated with new amount
+
+### Contribution Period (ContributionPeriod)
+
+```
+- id (UUID)
+- member_id (FK → members.id)
+- contribution_type_id (FK → contribution_types.id)
+- period_start (Date) - Calendar period start (01.01., 01.04., 01.07., 01.10., etc.)
+- status (Enum) - :unpaid (default), :paid, :suspended
+- amount (Decimal) - Amount at generation time (history when type changes)
+- notes (Text, optional) - Admin notes
+- timestamps
+```
+
+**Important:**
+
+- **NO** `period_end` - calculated from `period_start` + `interval`
+- **NO** `interval_type` - read from `contribution_type.interval`
+- Avoids redundancy and inconsistencies!
+
+**Calendar Period Logic:**
+
+- Monthly: 01.01. - 31.01., 01.02. - 28./29.02., etc.
+- Quarterly: 01.01. - 31.03., 01.04. - 30.06., 01.07. - 30.09., 01.10. - 31.12.
+- Half-yearly: 01.01. - 30.06., 01.07. - 31.12.
+- Yearly: 01.01. - 31.12.
+
+### Member (Extensions)
+
+```
+- contribution_type_id (FK → contribution_types.id, NOT NULL, default from settings)
+- contribution_start_date (Date, nullable) - When to start generating contributions
+- left_at (Date, nullable) - Exit date (existing)
+```
+
+**Logic for contribution_start_date:**
+
+- Auto-set based on global setting `include_joining_period`
+- If `include_joining_period = true`: First day of joining month/quarter/year
+- If `include_joining_period = false`: First day of NEXT period after joining
+- Can be manually overridden by admin
+
+**NO** `include_joining_period` field on Member - unnecessary due to `contribution_start_date`!
+
+### Global Settings
+
+```
+key: "contributions.include_joining_period"
+value: Boolean (Default: true)
+
+key: "contributions.default_contribution_type_id"
+value: UUID (Required) - Default contribution type for new members
+```
+
+**Meaning include_joining_period:**
+
+- `true`: Joining period is included (member pays from joining period)
+- `false`: Only from next full period after joining
+
+**Meaning default_contribution_type_id:**
+
+- Every new member automatically gets this contribution type
+- Must be configured in admin settings
+- Prevents: Members without contribution type
+
+---
+
+## Business Logic
+
+### Period Generation
+
+**Triggers:**
+
+- Member gets contribution type assigned (also during member creation)
+- New period begins (Cron job daily/weekly)
+- Admin requests manual regeneration
+
+**Algorithm:**
+
+1. Get `member.contribution_start_date` and `member.contribution_type`
+2. Calculate first period based on `contribution_start_date`
+3. Generate all periods from start to today (or `left_at` if present)
+4. Skip existing periods
+5. Set `amount` to current `contribution_type.amount`
+
+**Example (Yearly):**
+
+```
+Joining date: 15.03.2023
+include_joining_period: true
+→ contribution_start_date: 01.01.2023
+
+Generated periods:
+- 01.01.2023 - 31.12.2023 (joining period)
+- 01.01.2024 - 31.12.2024
+- 01.01.2025 - 31.12.2025 (current year)
+```
+
+**Example (Quarterly):**
+
+```
+Joining date: 15.03.2023
+include_joining_period: false
+→ contribution_start_date: 01.04.2023
+
+Generated periods:
+- 01.04.2023 - 30.06.2023 (first full quarter)
+- 01.07.2023 - 30.09.2023
+- 01.10.2023 - 31.12.2023
+- 01.01.2024 - 31.03.2024
+- ...
+```
+
+### Status Transitions
+
+```
+unpaid → paid
+unpaid → suspended
+paid → unpaid
+suspended → paid
+suspended → unpaid
+```
+
+**Permissions:**
+
+- Admin + Treasurer (Kassenwart) can change status
+- Uses existing permission system
+
+### Contribution Type Change
+
+**MVP - Same Interval Only:**
+
+- Member can only choose contribution type with **same interval**
+- Example: From "Regular (yearly)" to "Reduced (yearly)" ✓
+- Example: From "Regular (yearly)" to "Reduced (monthly)" ✗
+
+**Logic on Change:**
+
+1. Check: New contribution type has same interval
+2. If yes: Set `member.contribution_type_id`
+3. Future **unpaid** periods: Delete and regenerate with new amount
+4. Paid/suspended periods: Remain unchanged (historical amount)
+
+**Future - Different Intervals:**
+
+- Enable interval switching (e.g., yearly → monthly)
+- More complex logic for period overlaps
+- Needs additional validation
+
+### Member Exit
+
+**Logic:**
+
+- Periods only generated until `member.left_at`
+- Existing periods remain visible
+- Unpaid exit period can be marked as "suspended"
+
+**Example:**
+
+```
+Exit: 15.08.2024
+Yearly period: 01.01.2024 - 31.12.2024
+
+→ Period 2024 is shown (Status: unpaid)
+→ Admin can set to "suspended"
+→ No periods for 2025+ generated
+```
+
+---
+
+## UI/UX Design
+
+### Member List View
+
+**New Column: "Contribution Status"**
+
+**Default Display (Last Period):**
+
+- Shows status of **last completed** period
+- Example in 2024: Shows contribution for 2023
+- Color coding:
+ - Green: paid ✓
+ - Red: unpaid ✗
+ - Gray: suspended ⊘
+
+**Optional: Show Current Period**
+
+- Toggle: "Show current period" (2024)
+- Admin decides what to display
+
+**Filters:**
+
+- "Unpaid contributions in last period"
+- "Unpaid contributions in current period"
+
+### Member Detail View
+
+**Section: "Contributions"**
+
+**Contribution Type Assignment:**
+
+```
+┌─────────────────────────────────────┐
+│ Contribution Type: [Dropdown] │
+│ ⚠ Only types with same interval │
+│ can be selected │
+└─────────────────────────────────────┘
+```
+
+**Period Table:**
+
+```
+┌───────────────┬──────────┬────────┬──────────┬─────────┐
+│ Period │ Interval │ Amount │ Status │ Action │
+├───────────────┼──────────┼────────┼──────────┼─────────┤
+│ 01.01.2023- │ Yearly │ 50 € │ ☑ Paid │ │
+│ 31.12.2023 │ │ │ │ │
+├───────────────┼──────────┼────────┼──────────┼─────────┤
+│ 01.01.2024- │ Yearly │ 60 € │ ☐ Open │ [Mark │
+│ 31.12.2024 │ │ │ │ as paid]│
+├───────────────┼──────────┼────────┼──────────┼─────────┤
+│ 01.01.2025- │ Yearly │ 60 € │ ☐ Open │ [Mark │
+│ 31.12.2025 │ │ │ │ as paid]│
+└───────────────┴──────────┴────────┴──────────┴─────────┘
+
+Legend: ☑ = paid | ☐ = unpaid | ⊘ = suspended
+```
+
+**Quick Marking:**
+
+- Checkbox in each row for fast marking
+- Button: "Mark selected as paid/unpaid/suspended"
+- Bulk action for multiple periods
+
+### Admin: Contribution Types Management
+
+**List:**
+
+```
+┌────────────┬──────────┬──────────┬────────────┬─────────┐
+│ Name │ Amount │ Interval │ Members │ Actions │
+├────────────┼──────────┼──────────┼────────────┼─────────┤
+│ Regular │ 60 € │ Yearly │ 45 │ [Edit] │
+│ Reduced │ 30 € │ Yearly │ 12 │ [Edit] │
+│ Student │ 20 € │ Monthly │ 8 │ [Edit] │
+└────────────┴──────────┴──────────┴────────────┴─────────┘
+```
+
+**Edit:**
+
+- Name: ✓ editable
+- Amount: ✓ editable
+- Description: ✓ editable
+- Interval: ✗ **NOT** editable (grayed out)
+
+**Warning on Amount Change:**
+
+```
+⚠ Change amount to 65 €?
+
+Impact:
+- 45 members affected
+- Future unpaid periods will be generated with 65 €
+- Already paid periods remain with old amount
+
+[Cancel] [Confirm]
+```
+
+### Admin: Settings
+
+**Contribution Configuration:**
+
+```
+Default Contribution Type: [Dropdown: Contribution Types]
+
+Selected: "Regular (60 €, Yearly)"
+
+This contribution type is automatically assigned to all new members.
+Can be changed individually per member.
+
+---
+
+☐ Include joining period
+
+When active:
+Members pay from the period of their joining.
+
+Example (Yearly):
+Joining: 15.03.2023
+→ Pays from 2023
+
+When inactive:
+Members pay from the next full period.
+
+Example (Yearly):
+Joining: 15.03.2023
+→ Pays from 2024
+```
+
+---
+
+## Edge Cases
+
+### 1. Contribution Type Change with Different Interval
+
+**MVP:** Blocked (only same interval allowed)
+
+**UI:**
+
+```
+Error: Interval change not possible
+
+Current contribution type: "Regular (Yearly)"
+Selected contribution type: "Student (Monthly)"
+
+Changing the interval is currently not possible.
+Please select a contribution type with interval "Yearly".
+
+[OK]
+```
+
+**Future:**
+
+- Allow interval switching
+- Calculate overlaps
+- Generate new periods without duplicates
+
+### 2. Exit with Unpaid Contributions
+
+**Scenario:**
+
+```
+Member exits: 15.08.2024
+Yearly period 2024: unpaid
+```
+
+**UI Notice on Exit: (Low Prio)**
+
+```
+⚠ Unpaid contributions present
+
+This member has 1 unpaid period(s):
+- 2024: 60 € (unpaid)
+
+Do you want to continue?
+
+[ ] Mark contribution as "suspended"
+[Cancel] [Confirm Exit]
+```
+
+### 3. Multiple Unpaid Periods
+
+**Scenario:** Member hasn't paid for 2 years
+
+**Display:**
+
+```
+┌───────────────┬──────────┬────────┬──────────┬─────────┐
+│ 2023 │ Yearly │ 50 € │ ☐ Open │ [✓] │
+│ 2024 │ Yearly │ 60 € │ ☐ Open │ [✓] │
+│ 2025 │ Yearly │ 60 € │ ☐ Open │ [ ] │
+└───────────────┴──────────┴────────┴──────────┴─────────┘
+
+[Mark selected as paid/unpaid/suspended] (2 selected)
+```
+
+### 4. Amount Changes
+
+**Scenario:**
+
+```
+2023: Regular = 50 €
+2024: Regular = 60 € (increase)
+```
+
+**Result:**
+
+- Period 2023: Saved with 50 € (history)
+- Period 2024: Generated with 60 € (current)
+- Both periods show correct historical amount
+
+### 5. Date Boundaries
+
+**Problem:** What if today = 01.01.2025?
+
+**Solution:**
+
+- Current period (2025) is generated
+- Status: unpaid (open)
+- Shown in overview
+
+---
+
+## Implementation Scope
+
+### MVP (Phase 1)
+
+**Included:**
+
+- ✓ Contribution types (CRUD)
+- ✓ Automatic period generation
+- ✓ Status management (paid/unpaid/suspended)
+- ✓ Member overview with contribution status
+- ✓ Period view per member
+- ✓ Quick checkbox marking
+- ✓ Bulk actions
+- ✓ Amount history
+- ✓ Same-interval type change
+- ✓ Default contribution type
+- ✓ Joining period configuration
+
+**NOT Included:**
+
+- ✗ Interval change (only same interval)
+- ✗ Payment details (date, method)
+- ✗ Automatic integration (vereinfacht.digital)
+- ✗ Prorata calculation
+- ✗ Reports/statistics
+- ✗ Reminders/dunning (manual via filters)
+
+### Future Enhancements
+
+**Phase 2:**
+
+- Payment details (date, amount, method)
+- Interval change for future unpaid periods
+- Manual vereinfacht.digital links per member
+- Extended filter options
+
+**Phase 3:**
+
+- Automated vereinfacht.digital integration
+- Automatic payment matching
+- SEPA integration
+- Advanced reports
diff --git a/docs/feature-roadmap.md b/docs/feature-roadmap.md
index 609523c..2f86f5e 100644
--- a/docs/feature-roadmap.md
+++ b/docs/feature-roadmap.md
@@ -187,10 +187,16 @@
**Current State:**
- ✅ Basic "paid" boolean field on members
+- ✅ **UI Mock-ups for Contribution Types & Settings** (2025-12-02)
- ⚠️ No payment tracking
**Open Issues:**
- [#156](https://git.local-it.org/local-it/mitgliederverwaltung/issues/156) - Set up & document testing environment for vereinfacht.digital (L, Low priority)
+- [#226](https://git.local-it.org/local-it/mitgliederverwaltung/issues/226) - Payment/Contribution Mockup Pages (Preview)
+
+**Mock-Up Pages (Non-Functional Preview):**
+- `/contribution_types` - Contribution Types Management
+- `/contribution_settings` - Global Contribution Settings
**Missing Features:**
- ❌ Membership fee configuration
diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex
index 7ff7f25..1c1138e 100644
--- a/lib/mv_web/components/layouts/navbar.ex
+++ b/lib/mv_web/components/layouts/navbar.ex
@@ -25,6 +25,17 @@ defmodule MvWeb.Layouts.Navbar do
diff --git a/lib/mv_web/endpoint.ex b/lib/mv_web/endpoint.ex
index 97dcae4..d1b4247 100644
--- a/lib/mv_web/endpoint.ex
+++ b/lib/mv_web/endpoint.ex
@@ -39,6 +39,11 @@ defmodule MvWeb.Endpoint do
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :mv
end
+ # Enable Ecto SQL Sandbox in test environment for async tests
+ if Application.compile_env(:mv, :sql_sandbox) do
+ plug Phoenix.Ecto.SQL.Sandbox
+ end
+
plug Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"
diff --git a/lib/mv_web/live/contribution_period_live/show.ex b/lib/mv_web/live/contribution_period_live/show.ex
new file mode 100644
index 0000000..95179ac
--- /dev/null
+++ b/lib/mv_web/live/contribution_period_live/show.ex
@@ -0,0 +1,345 @@
+defmodule MvWeb.ContributionPeriodLive.Show do
+ @moduledoc """
+ Mock-up LiveView for Member Contribution Periods (Admin/Treasurer View).
+
+ This is a preview-only page that displays the planned UI for viewing
+ and managing contribution periods for a specific member.
+ It shows static mock data and is not functional.
+
+ ## Planned Features (Future Implementation)
+ - Display all contribution periods for a member
+ - Show period dates, interval, amount, and status
+ - Quick status change (paid/unpaid/suspended)
+ - Bulk marking of multiple periods
+ - Notes per period
+
+ ## Note
+ This page is intentionally non-functional and serves as a UI mockup
+ for the upcoming Membership Contributions feature.
+ """
+ use MvWeb, :live_view
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok,
+ socket
+ |> assign(:page_title, gettext("Member Contributions"))
+ |> assign(:member, mock_member())
+ |> assign(:periods, mock_periods())
+ |> assign(:selected_periods, MapSet.new())}
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.mockup_warning />
+
+ <.header>
+ {gettext("Contributions for %{name}", name: "#{@member.first_name} #{@member.last_name}")}
+ <:subtitle>
+ {gettext("Contribution type")}:
+ {@member.contribution_type}
+ · {gettext("Member since")}: {@member.joined_at}
+
+ <:actions>
+ <.link navigate={~p"/contribution_settings"} class="btn btn-ghost btn-sm">
+ <.icon name="hero-arrow-left" class="size-4" />
+ {gettext("Back to Settings")}
+
+
+
+
+ <%!-- Member Info Card --%>
+
+ {gettext(
+ "See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
+ )}
+
+ {gettext(
+ "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
+ )}
+
+
+
+ {gettext("Name & Amount")}
+ - {gettext("Can be changed at any time. Amount changes affect future periods only.")}
+
+
+ {gettext("Interval")}
+ - {gettext(
+ "Fixed after creation. Members can only switch between types with the same interval."
+ )}
+
+
+ {gettext("Deletion")}
+ - {gettext("Only possible if no members are assigned to this type.")}
+
+
+
+
+
+ """
+ end
+
+ # Mock data for demonstration
+ defp mock_contribution_types do
+ [
+ %{
+ id: "1",
+ name: gettext("Regular"),
+ description: gettext("Standard membership fee for regular members"),
+ amount: Decimal.new("60.00"),
+ interval: :yearly,
+ member_count: 45
+ },
+ %{
+ id: "2",
+ name: gettext("Reduced"),
+ description: gettext("Reduced fee for unemployed, pensioners, or low income"),
+ amount: Decimal.new("30.00"),
+ interval: :yearly,
+ member_count: 12
+ },
+ %{
+ id: "3",
+ name: gettext("Student"),
+ description: gettext("Monthly fee for students and trainees"),
+ amount: Decimal.new("5.00"),
+ interval: :monthly,
+ member_count: 8
+ },
+ %{
+ id: "4",
+ name: gettext("Family"),
+ description: gettext("Quarterly fee for family memberships"),
+ amount: Decimal.new("25.00"),
+ interval: :quarterly,
+ member_count: 15
+ },
+ %{
+ id: "5",
+ name: gettext("Supporting Member"),
+ description: gettext("Half-yearly contribution for supporting members"),
+ amount: Decimal.new("100.00"),
+ interval: :half_yearly,
+ member_count: 3
+ },
+ %{
+ id: "6",
+ name: gettext("Honorary"),
+ description: gettext("No fee for honorary members"),
+ amount: Decimal.new("0.00"),
+ interval: :yearly,
+ member_count: 2
+ }
+ ]
+ end
+
+ defp format_currency(%Decimal{} = amount) do
+ "#{Decimal.to_string(amount)} €"
+ end
+
+ defp format_interval(:monthly), do: gettext("Monthly")
+ defp format_interval(:quarterly), do: gettext("Quarterly")
+ defp format_interval(:half_yearly), do: gettext("Half-yearly")
+ defp format_interval(:yearly), do: gettext("Yearly")
+end
diff --git a/lib/mv_web/live/member_live/form.ex b/lib/mv_web/live/member_live/form.ex
index 5370154..5380d0f 100644
--- a/lib/mv_web/live/member_live/form.ex
+++ b/lib/mv_web/live/member_live/form.ex
@@ -5,80 +5,212 @@ defmodule MvWeb.MemberLive.Form do
## Features
- Create new members with personal information
- Edit existing member details
- - Manage custom properties (dynamic fields)
+ - Grouped sections for better organization
+ - Tab navigation (Payments tab disabled, coming soon)
+ - Manage custom properties (dynamic fields, displayed sorted by name)
- Real-time validation with visual feedback
- - Link/unlink user accounts
- ## Form Fields
- **Required:**
- - first_name, last_name, email
-
- **Optional:**
- - phone_number, address fields (city, street, house_number, postal_code)
- - join_date, exit_date
- - paid status
- - notes
-
- ## Custom Field Values
- Members can have dynamic custom field values defined by CustomFields.
- The form dynamically renders inputs based on available CustomFields.
+ ## Form Sections
+ - Personal Data: Name, address, contact information, membership dates, notes
+ - Custom Fields: Dynamic fields in uniform grid layout (displayed sorted by name)
+ - Payment Data: Mockup section (not editable)
## Events
- `validate` - Real-time form validation
- `save` - Submit form (create or update member)
- - Custom field value management events for adding/removing custom fields
"""
use MvWeb, :live_view
@impl true
def render(assigns) do
+ # Sort custom fields by name for display only
+ sorted_custom_fields = Enum.sort_by(assigns.custom_fields, & &1.name)
+ assigns = assign(assigns, :sorted_custom_fields, sorted_custom_fields)
+
~H"""
- <.header>
- {@page_title}
- <:subtitle>
- {gettext("Fields marked with an asterisk (*) cannot be empty.")}
-
-
-
<.form for={@form} id="member-form" phx-change="validate" phx-submit="save">
- <.input field={@form[:first_name]} label={gettext("First Name")} required />
- <.input field={@form[:last_name]} label={gettext("Last Name")} required />
- <.input field={@form[:email]} label={gettext("Email")} required type="email" />
- <.input field={@form[:paid]} label={gettext("Paid")} type="checkbox" />
- <.input field={@form[:phone_number]} label={gettext("Phone Number")} />
- <.input field={@form[:join_date]} label={gettext("Join Date")} type="date" />
- <.input field={@form[:exit_date]} label={gettext("Exit Date")} type="date" />
- <.input field={@form[:notes]} label={gettext("Notes")} />
- <.input field={@form[:city]} label={gettext("City")} />
- <.input field={@form[:street]} label={gettext("Street")} />
- <.input field={@form[:house_number]} label={gettext("House Number")} />
- <.input field={@form[:postal_code]} label={gettext("Postal Code")} />
+ <%!-- Header with Back button, Name display, and Save button --%>
+
+ <%!-- Render in sorted order by finding the form for each sorted custom field --%>
+ <%= for cf <- @sorted_custom_fields do %>
+ <.inputs_for :let={f_cfv} field={@form[:custom_field_values]}>
+ <%= if f_cfv[:custom_field_id].value == cf.id do %>
+
"""
@@ -106,8 +238,8 @@ defmodule MvWeb.MemberLive.Form do
id -> Ash.get!(Mv.Membership.Member, id)
end
- action = if is_nil(member), do: "New", else: "Edit"
- page_title = action <> " " <> "Member"
+ page_title =
+ if is_nil(member), do: gettext("Create Member"), else: gettext("Edit Member")
{:ok,
socket
@@ -213,5 +345,37 @@ defmodule MvWeb.MemberLive.Form do
end
defp return_path("index", _member), do: ~p"/members"
+ defp return_path("show", nil), do: ~p"/members"
defp return_path("show", member), do: ~p"/members/#{member.id}"
+
+ # -----------------------------------------------------------------
+ # Helper Components
+ # -----------------------------------------------------------------
+
+ # Renders a form section box with border and title.
+ attr :title, :string, required: true
+ slot :inner_block, required: true
+
+ defp form_section(assigns) do
+ ~H"""
+
+
{@title}
+
+ {render_slot(@inner_block)}
+
+
+ """
+ end
+
+ # -----------------------------------------------------------------
+ # Helper Functions for Custom Fields
+ # -----------------------------------------------------------------
+
+ # Returns input type for custom field based on value type
+ defp custom_field_input_type(:string), do: "text"
+ defp custom_field_input_type(:integer), do: "number"
+ defp custom_field_input_type(:boolean), do: "checkbox"
+ defp custom_field_input_type(:date), do: "date"
+ defp custom_field_input_type(:email), do: "email"
+ defp custom_field_input_type(_), do: "text"
end
diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex
index 7601f46..d84fca4 100644
--- a/lib/mv_web/live/member_live/show.ex
+++ b/lib/mv_web/live/member_live/show.ex
@@ -3,19 +3,16 @@ defmodule MvWeb.MemberLive.Show do
LiveView for displaying a single member's details.
## Features
- - Display all member information (personal, contact, address)
- - Show linked user account (if exists)
- - Display custom field values
+ - Display all member information in grouped sections
+ - Tab navigation for future features (Payments)
+ - Show custom field values with type-based formatting
- Navigate to edit form
- Return to member list
- ## Displayed Information
- - Basic: name, email, dates (join, exit)
- - Contact: phone number
- - Address: street, house number, postal code, city
- - Status: paid flag
- - Relationships: linked user account
- - Custom: dynamic custom field values from CustomFields
+ ## Sections
+ - Personal Data: Name, address, contact information, membership dates, notes
+ - Custom Fields: Dynamic fields in uniform grid layout (sorted by name)
+ - Payment Data: Mockup section with placeholder data
## Navigation
- Back to member list
@@ -23,69 +20,155 @@ defmodule MvWeb.MemberLive.Show do
"""
use MvWeb, :live_view
import Ash.Query
- alias MvWeb.Helpers.DateFormatter
@impl true
def render(assigns) do
~H"""
- <.header>
- {@member.first_name} {@member.last_name}
- <:subtitle>{gettext("This is a member record from your database.")}
+ <%!-- Header with Back button, Name, and Edit button --%>
+
+ <.button navigate={~p"/members"} aria-label={gettext("Back to members list")}>
+ <.icon name="hero-arrow-left" class="size-4" />
+ {gettext("Back")}
+
- <:actions>
- <.button navigate={~p"/members"} aria-label={gettext("Back to members list")}>
- <.icon name="hero-arrow-left" />
- {gettext("Back to members list")}
-
- <.button variant="primary" navigate={~p"/members/#{@member}/edit?return_to=show"}>
- <.icon name="hero-pencil-square" /> {gettext("Edit Member")}
-
-
-
+
"""
end
@@ -113,16 +196,119 @@ defmodule MvWeb.MemberLive.Show do
defp page_title(:show), do: gettext("Show Member")
defp page_title(:edit), do: gettext("Edit Member")
- defp format_custom_field_value(cfv) do
- value =
- case cfv.value do
- %{value: v} -> v
- v -> v
- end
+ # -----------------------------------------------------------------
+ # Helper Components
+ # -----------------------------------------------------------------
- case value do
- %Date{} = date -> DateFormatter.format_date(date)
- other -> other
+ # Renders a section box with border and title.
+ attr :title, :string, required: true
+ slot :inner_block, required: true
+
+ defp section_box(assigns) do
+ ~H"""
+
+