Compare commits

..

22 commits

Author SHA1 Message Date
2542bcf9e4
fix: improve gettext translations and deduplicate email formatting in member views
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-12-03 15:30:09 +01:00
ed961f7585
Redesign member view/edit UI with improved accessibility
- Group fields into Personal Data, Custom Fields, and Payment Data sections
- Fix WCAG AA contrast issues and semantic HTML (dt/dd in dl)
- Format mailto links with member name in href attribute
2025-12-03 15:29:29 +01:00
82f1a65b85 Merge pull request 'Fix postgres errors when running tests' (#245) from fix-test-postgres-errors into main
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #245
Reviewed-by: moritz <moritz@noreply.git.local-it.org>
2025-12-03 14:51:08 +01:00
dfff2486b5
Fix postgres errors when running tests
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 14:47:58 +01:00
aaea6a01e2 Merge pull request 'Implement UI fields to mock payment concept closes #226' (#241) from feature/226_payment_mockup into main
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is passing
Reviewed-on: #241
2025-12-03 14:35:14 +01:00
4057b2d631
Extend gettext conflict script to other conflict marker styles 2025-12-03 14:32:46 +01:00
cd1af5aff5 feat: Add contribution management mock-up pages
Add non-functional preview pages for Contribution Types, Settings, and Member Contribution Periods with German translations
2025-12-03 14:32:09 +01:00
8391122426 resolve review issues 2025-12-03 14:32:09 +01:00
a5aeef3e27 docs: payment concept 2025-12-03 14:32:09 +01:00
422cf37a1e Merge pull request 'Fix UI issues' (#242) from ui-fixes into main
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #242
Reviewed-by: simon <s.thiessen@local-it.org>
2025-12-03 14:30:13 +01:00
a10d42f1ed Merge pull request 'Add file_envs for secrets and allow passing database url via separate envs' (#246) from add-file-envs into main
Some checks reported errors
continuous-integration/drone/push Build was killed
Reviewed-on: #246
Reviewed-by: rafael <rafael@noreply.git.local-it.org>
2025-12-03 14:29:32 +01:00
d1bab1288c
Merge remote-tracking branch 'origin/main' into add-file-envs
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-12-03 14:29:04 +01:00
1623b63207
fix: resolve review comments
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-12-03 14:27:22 +01:00
e6c5a58c65
Show dates in european format
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-12-03 14:20:14 +01:00
ee414c9440
Hide OIDC ID and ID columns for users 2025-12-03 14:20:14 +01:00
366d4c104a
Prevent tables from growing the page horizontally 2025-12-03 14:20:14 +01:00
09c75212b2
chore: add remove-gettext-conflicts to Justfile
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 13:46:55 +01:00
ce15b8f59b
fix: mailto formatting
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 12:54:49 +01:00
d8384098b4
chore: update prod-compose to use file-envs for secrets
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 12:38:24 +01:00
ee094eec2f
feat: add file env support for secrets 2025-12-03 12:36:13 +01:00
eedd24b93c
Truncate long entries in tables to prevent height changes 2025-12-02 16:33:15 +01:00
06ba50f05d
Fix translation "Bearbeite" -> "Bearbeiten" 2025-12-02 16:32:55 +01:00
35 changed files with 3817 additions and 314 deletions

3
.gitignore vendored
View file

@ -41,3 +41,6 @@ npm-debug.log
.env
.elixir_ls/
# Docker secrets directory (generated by `just init-secrets`)
/secrets/

View file

@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- CopyToClipboard JavaScript hook with fallback for older browsers
- Button shows count of visible selected members (respects search/filter)
- 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)
### Fixed
- Email validation false positive when linking user and member with identical emails (#168 Problem #4)

View file

@ -85,3 +85,32 @@ clean:
mix clean
rm -rf .elixir_ls
rm -rf _build
# Remove Git merge conflict markers from gettext files
remove-gettext-conflicts:
#!/usr/bin/env bash
set -euo pipefail
find priv/gettext -type f -exec sed -i '/^<<<<<<</d; /^=======$/d; /^>>>>>>>/d; /^%%%%%%%/d; /^++++++/d; s/^+//' {} \;
# Production environment commands
# ================================
# Initialize secrets directory with generated secrets (only if not exists)
init-prod-secrets:
#!/usr/bin/env bash
set -euo pipefail
if [ -d "secrets" ]; then
echo "Secrets directory already exists. Skipping generation."
exit 0
fi
echo "Creating secrets directory and generating secrets..."
mkdir -p secrets
mix phx.gen.secret > secrets/secret_key_base.txt
mix phx.gen.secret > secrets/token_signing_secret.txt
openssl rand -base64 32 | tr -d '\n' > secrets/db_password.txt
touch secrets/oidc_client_secret.txt
echo "Secrets generated in ./secrets/"
# Start production environment with Docker Compose
start-prod: init-prod-secrets
docker compose -f docker-compose.prod.yml up -d

View file

@ -217,6 +217,13 @@ For testing the production Docker build locally:
# OIDC_BASE_URL=http://localhost:8080/auth/v1
# OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback
# OIDC_CLIENT_SECRET=<from-rauthy-client>
# Alternative: Use _FILE variables for Docker secrets (takes priority over regular vars):
# SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base
# TOKEN_SIGNING_SECRET_FILE=/run/secrets/token_signing_secret
# OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc_client_secret
# DATABASE_URL_FILE=/run/secrets/database_url
# DATABASE_PASSWORD_FILE=/run/secrets/database_password
```
3. **Start development environment** (for Rauthy):
@ -250,7 +257,7 @@ For actual production deployment:
- Set `OIDC_BASE_URL` to your production OIDC provider
- Configure proper Docker networks
3. **Set up SSL/TLS** (e.g., via reverse proxy like Nginx/Traefik)
4. **Use secure secrets management** (environment variables, Docker secrets, vault)
4. **Use secure secrets management** — All sensitive environment variables support a `_FILE` suffix for Docker secrets (e.g., `SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base`). See `docker-compose.prod.yml` for an example setup with Docker secrets.
5. **Configure database backups**

View file

@ -7,6 +7,75 @@ import Config
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.
# Helper function to read environment variables with Docker secrets support.
# Supports the _FILE suffix pattern: if VAR_FILE is set, reads the value from
# that file path. Otherwise falls back to VAR directly.
# VAR_FILE takes priority and must contain the full absolute path to the secret file.
get_env_or_file = fn var_name, default ->
file_var = "#{var_name}_FILE"
case System.get_env(file_var) do
nil ->
System.get_env(var_name, default)
file_path ->
case File.read(file_path) do
{:ok, content} ->
String.trim_trailing(content)
{:error, reason} ->
raise """
Failed to read secret from file specified in #{file_var}="#{file_path}".
Error: #{inspect(reason)}
"""
end
end
end
# Same as get_env_or_file but raises if the value is not set
get_env_or_file! = fn var_name, error_message ->
case get_env_or_file.(var_name, nil) do
nil -> raise error_message
value -> value
end
end
# Build database URL from individual components or use DATABASE_URL directly.
# Supports both approaches:
# 1. DATABASE_URL (or DATABASE_URL_FILE) - full connection URL
# 2. Separate vars: DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD (or _FILE), DATABASE_NAME, DATABASE_PORT
build_database_url = fn ->
case get_env_or_file.("DATABASE_URL", nil) do
nil ->
# Build URL from separate components
host =
System.get_env("DATABASE_HOST") ||
raise "DATABASE_HOST is required when DATABASE_URL is not set"
user =
System.get_env("DATABASE_USER") ||
raise "DATABASE_USER is required when DATABASE_URL is not set"
password =
get_env_or_file!.("DATABASE_PASSWORD", """
DATABASE_PASSWORD or DATABASE_PASSWORD_FILE is required when DATABASE_URL is not set.
""")
database =
System.get_env("DATABASE_NAME") ||
raise "DATABASE_NAME is required when DATABASE_URL is not set"
port = System.get_env("DATABASE_PORT", "5432")
# URL-encode the password to handle special characters
encoded_password = URI.encode_www_form(password)
"ecto://#{user}:#{encoded_password}@#{host}:#{port}/#{database}"
url ->
url
end
end
# ## Using releases
#
# If you use `mix release`, you need to explicitly enable the server
@ -21,12 +90,7 @@ if System.get_env("PHX_SERVER") do
end
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
database_url = build_database_url.()
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
@ -41,12 +105,12 @@ if config_env() == :prod do
# want to use a different value for prod and you most likely don't want
# to check this value into version control, so we use an environment
# variable instead.
# Supports SECRET_KEY_BASE or SECRET_KEY_BASE_FILE for Docker secrets.
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
get_env_or_file!.("SECRET_KEY_BASE", """
environment variable SECRET_KEY_BASE (or SECRET_KEY_BASE_FILE) is missing.
You can generate one by calling: mix phx.gen.secret
"""
""")
host = System.get_env("PHX_HOST") || raise "Please define the PHX_HOST environment variable."
port = String.to_integer(System.get_env("PORT") || "4000")
@ -54,32 +118,47 @@ if config_env() == :prod do
config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
# Rauthy OIDC configuration
# Supports OIDC_CLIENT_SECRET or OIDC_CLIENT_SECRET_FILE for Docker secrets.
# OIDC_CLIENT_SECRET is required only if OIDC is being used (indicated by explicit OIDC env vars).
oidc_base_url = System.get_env("OIDC_BASE_URL")
oidc_client_id = System.get_env("OIDC_CLIENT_ID")
oidc_in_use = not is_nil(oidc_base_url) or not is_nil(oidc_client_id)
client_secret =
if oidc_in_use do
get_env_or_file!.("OIDC_CLIENT_SECRET", """
environment variable OIDC_CLIENT_SECRET (or OIDC_CLIENT_SECRET_FILE) is missing.
This is required when OIDC authentication is configured (OIDC_BASE_URL or OIDC_CLIENT_ID is set).
""")
else
get_env_or_file.("OIDC_CLIENT_SECRET", nil)
end
config :mv, :rauthy,
client_id: System.get_env("OIDC_CLIENT_ID") || "mv",
base_url: System.get_env("OIDC_BASE_URL") || "http://localhost:8080/auth/v1",
client_secret: System.get_env("OIDC_CLIENT_SECRET"),
client_id: oidc_client_id || "mv",
base_url: oidc_base_url || "http://localhost:8080/auth/v1",
client_secret: client_secret,
redirect_uri:
System.get_env("OIDC_REDIRECT_URI") || "http://#{host}:#{port}/auth/user/rauthy/callback"
# Token signing secret from environment variable
# This overrides the placeholder value set in prod.exs
# Supports TOKEN_SIGNING_SECRET or TOKEN_SIGNING_SECRET_FILE for Docker secrets.
token_signing_secret =
System.get_env("TOKEN_SIGNING_SECRET") ||
raise """
environment variable TOKEN_SIGNING_SECRET is missing.
get_env_or_file!.("TOKEN_SIGNING_SECRET", """
environment variable TOKEN_SIGNING_SECRET (or TOKEN_SIGNING_SECRET_FILE) is missing.
You can generate one by calling: mix phx.gen.secret
"""
""")
config :mv, :token_signing_secret, token_signing_secret
config :mv, MvWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"],
http: [
# Enable IPv6 and bind on all interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
# Bind on all IPv4 interfaces.
# Use {0, 0, 0, 0, 0, 0, 0, 0} for IPv6, or {127, 0, 0, 1} for localhost only.
# See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
ip: {0, 0, 0, 0, 0, 0, 0, 0},
ip: {0, 0, 0, 0},
port: port
],
secret_key_base: secret_key_base,

View file

@ -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

View file

@ -2,21 +2,32 @@ services:
app:
image: git.local-it.org/local-it/mitgliederverwaltung:latest
container_name: mv-prod-app
# Use host network for local testing to access localhost:8080 (Rauthy)
# In real production, remove this and use external OIDC provider
network_mode: host
ports:
- "4001:4001"
environment:
DATABASE_URL: "ecto://postgres:postgres@localhost:5001/mv_prod"
SECRET_KEY_BASE: "${SECRET_KEY_BASE}"
TOKEN_SIGNING_SECRET: "${TOKEN_SIGNING_SECRET}"
PHX_HOST: "${PHX_HOST}"
# Database configuration using separate variables
# Use Docker service name for internal networking
DATABASE_HOST: "db-prod"
DATABASE_PORT: "5432"
DATABASE_USER: "postgres"
DATABASE_NAME: "mv_prod"
DATABASE_PASSWORD_FILE: "/run/secrets/db_password"
# Phoenix secrets via Docker secrets
SECRET_KEY_BASE_FILE: "/run/secrets/secret_key_base"
TOKEN_SIGNING_SECRET_FILE: "/run/secrets/token_signing_secret"
PHX_HOST: "${PHX_HOST:-localhost}"
PORT: "4001"
PHX_SERVER: "true"
# Rauthy OIDC config - uses localhost because of host network mode
# Rauthy OIDC config - use host.docker.internal to reach host services
OIDC_CLIENT_ID: "mv"
OIDC_BASE_URL: "http://localhost:8080/auth/v1"
OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET:-}"
OIDC_BASE_URL: "http://host.docker.internal:8080/auth/v1"
OIDC_CLIENT_SECRET_FILE: "/run/secrets/oidc_client_secret"
OIDC_REDIRECT_URI: "http://localhost:4001/auth/user/rauthy/callback"
secrets:
- db_password
- secret_key_base
- token_signing_secret
- oidc_client_secret
depends_on:
- db-prod
restart: unless-stopped
@ -26,13 +37,25 @@ services:
container_name: mv-prod-db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: mv_prod
secrets:
- db_password
volumes:
- postgres_data_prod:/var/lib/postgresql/data
ports:
- "5001:5432"
restart: unless-stopped
secrets:
db_password:
file: ./secrets/db_password.txt
secret_key_base:
file: ./secrets/secret_key_base.txt
token_signing_secret:
file: ./secrets/token_signing_secret.txt
oidc_client_secret:
file: ./secrets/oidc_client_secret.txt
volumes:
postgres_data_prod:

View file

@ -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**

View file

@ -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

View file

@ -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

View file

@ -79,7 +79,7 @@ defmodule MvWeb.CoreComponents do
<p>{msg}</p>
</div>
<div class="flex-1" />
<button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}>
<button type="button" class="self-start cursor-pointer group" aria-label={gettext("close")}>
<.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
</button>
</div>
@ -368,6 +368,7 @@ defmodule MvWeb.CoreComponents do
end
~H"""
<div class="overflow-auto">
<table class="table table-zebra">
<thead>
<tr>
@ -392,14 +393,14 @@ defmodule MvWeb.CoreComponents do
<td
:for={col <- @col}
phx-click={@row_click && @row_click.(row)}
class={@row_click && "hover:cursor-pointer"}
class={["max-w-xs truncate", @row_click && "hover:cursor-pointer"]}
>
{render_slot(col, @row_item.(row))}
</td>
<td
:for={dyn_col <- @dynamic_cols}
phx-click={@row_click && @row_click.(row)}
class={@row_click && "hover:cursor-pointer"}
class={["max-w-xs truncate", @row_click && "hover:cursor-pointer"]}
>
{if dyn_col[:render] do
rendered = dyn_col[:render].(@row_item.(row))
@ -423,6 +424,7 @@ defmodule MvWeb.CoreComponents do
</tr>
</tbody>
</table>
</div>
"""
end

View file

@ -25,6 +25,17 @@ defmodule MvWeb.Layouts.Navbar do
<li><.link navigate="/members">{gettext("Members")}</.link></li>
<li><.link navigate="/custom_fields">{gettext("Custom Fields")}</.link></li>
<li><.link navigate="/users">{gettext("Users")}</.link></li>
<li>
<details>
<summary>{gettext("Contributions")}</summary>
<ul class="bg-base-200 rounded-t-none p-2 z-10 w-48">
<li><.link navigate="/contribution_types">{gettext("Contribution Types")}</.link></li>
<li>
<.link navigate="/contribution_settings">{gettext("Contribution Settings")}</.link>
</li>
</ul>
</details>
</li>
</ul>
</div>
<div class="flex gap-2">

View file

@ -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"

View file

@ -0,0 +1,27 @@
defmodule MvWeb.Helpers.DateFormatter do
@moduledoc """
Centralized date formatting helper for the application.
Formats dates in European format (dd.mm.yyyy).
"""
use Gettext, backend: MvWeb.Gettext
@doc """
Formats a Date struct to European format (dd.mm.yyyy).
## Examples
iex> MvWeb.Helpers.DateFormatter.format_date(~D[2024-03-15])
"15.03.2024"
iex> MvWeb.Helpers.DateFormatter.format_date(nil)
""
"""
def format_date(%Date{} = date) do
Calendar.strftime(date, "%d.%m.%Y")
end
def format_date(nil), do: ""
def format_date(_), do: "Invalid date"
end

View file

@ -19,7 +19,7 @@ defmodule MvWeb.Components.SortHeaderComponent do
@impl true
def render(assigns) do
~H"""
<div class="tooltip" data-tip={aria_sort(@field, @sort_field, @sort_order)}>
<div class="tooltip tooltip-bottom" data-tip={aria_sort(@field, @sort_field, @sort_order)}>
<button
type="button"
aria-label={aria_sort(@field, @sort_field, @sort_order)}

View file

@ -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"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.mockup_warning />
<.header>
{gettext("Contributions for %{name}", name: "#{@member.first_name} #{@member.last_name}")}
<:subtitle>
{gettext("Contribution type")}:
<span class="font-semibold">{@member.contribution_type}</span>
· {gettext("Member since")}: <span class="font-mono">{@member.joined_at}</span>
</:subtitle>
<:actions>
<.link navigate={~p"/contribution_settings"} class="btn btn-ghost btn-sm">
<.icon name="hero-arrow-left" class="size-4" />
{gettext("Back to Settings")}
</.link>
</:actions>
</.header>
<%!-- Member Info Card --%>
<div class="mb-6 shadow card bg-base-100">
<div class="card-body">
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
<div>
<span class="text-sm text-base-content/60">{gettext("Email")}</span>
<p class="font-medium">{@member.email}</p>
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Contribution Start")}</span>
<p class="font-mono">{@member.contribution_start}</p>
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Total Contributions")}</span>
<p class="font-semibold">{length(@periods)}</p>
</div>
<div>
<span class="text-sm text-base-content/60">{gettext("Open Contributions")}</span>
<p class="font-semibold text-error">
{Enum.count(@periods, &(&1.status == :unpaid))}
</p>
</div>
</div>
</div>
</div>
<%!-- Contribution Type Change --%>
<div class="mb-6 card bg-base-200">
<div class="py-4 card-body">
<div class="flex flex-wrap items-center gap-4">
<span class="font-semibold">{gettext("Change Contribution Type")}:</span>
<select class="w-64 select select-bordered select-sm" disabled>
<option selected>{@member.contribution_type} (60,00 , {gettext("Yearly")})</option>
<option>{gettext("Reduced")} (30,00 , {gettext("Yearly")})</option>
<option>{gettext("Honorary")} (0,00 , {gettext("Yearly")})</option>
</select>
<span
class="text-sm text-base-content/60 cursor-help tooltip tooltip-bottom"
data-tip={
gettext(
"Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
)
}
>
<.icon name="hero-question-mark-circle" class="inline size-4" />
{gettext("Why are not all contribution types shown?")}
</span>
</div>
</div>
</div>
<%!-- Bulk Actions --%>
<div class="flex flex-wrap items-center gap-4 mb-4">
<span class="text-sm text-base-content/60">
{ngettext(
"%{count} period selected",
"%{count} periods selected",
MapSet.size(@selected_periods),
count: MapSet.size(@selected_periods)
)}
</span>
<button class="btn btn-sm btn-success" disabled>
<.icon name="hero-check" class="size-4" />
{gettext("Mark as Paid")}
</button>
<button class="btn btn-sm btn-ghost" disabled>
<.icon name="hero-minus-circle" class="size-4" />
{gettext("Mark as Suspended")}
</button>
<button class="btn btn-sm btn-ghost" disabled>
<.icon name="hero-x-circle" class="size-4" />
{gettext("Mark as Unpaid")}
</button>
</div>
<%!-- Periods Table --%>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>
<input type="checkbox" class="checkbox checkbox-sm" disabled />
</th>
<th>{gettext("Time Period")}</th>
<th>{gettext("Interval")}</th>
<th>{gettext("Amount")}</th>
<th>{gettext("Status")}</th>
<th>{gettext("Notes")}</th>
<th>{gettext("Actions")}</th>
</tr>
</thead>
<tbody>
<tr :for={period <- @periods} class={period_row_class(period.status)}>
<td>
<input
type="checkbox"
class="checkbox checkbox-sm"
checked={MapSet.member?(@selected_periods, period.id)}
disabled
/>
</td>
<td>
<div class="font-mono">
{period.period_start} {period.period_end}
</div>
<div :if={period.is_current} class="mt-1 badge badge-info badge-sm">
{gettext("Current")}
</div>
</td>
<td>
<span class="badge badge-outline badge-sm">{format_interval(period.interval)}</span>
</td>
<td>
<span class="font-mono">{format_currency(period.amount)}</span>
</td>
<td>
<.status_badge status={period.status} />
</td>
<td>
<span :if={period.notes} class="text-sm italic text-base-content/60">
{period.notes}
</span>
<span :if={!period.notes} class="text-base-content/30"></span>
</td>
<td class="w-0 font-semibold whitespace-nowrap">
<div class="flex gap-4">
<.link
href="#"
class={[
"cursor-not-allowed",
if(period.status == :paid, do: "invisible", else: "opacity-50")
]}
>
{gettext("Paid")}
</.link>
<.link
href="#"
class={[
"cursor-not-allowed",
if(period.status == :suspended, do: "invisible", else: "opacity-50")
]}
>
{gettext("Suspend")}
</.link>
<.link
href="#"
class={[
"cursor-not-allowed",
if(period.status != :paid, do: "invisible", else: "opacity-50")
]}
>
{gettext("Reopen")}
</.link>
<.link href="#" class="opacity-50 cursor-not-allowed">
{gettext("Note")}
</.link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</Layouts.app>
"""
end
# Mock-up warning banner component - subtle orange style
defp mockup_warning(assigns) do
~H"""
<div class="flex items-center gap-3 px-4 py-3 mb-6 border rounded-lg border-warning text-warning bg-base-100">
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
<div>
<span class="font-semibold">{gettext("Preview Mockup")}</span>
<span class="ml-2 text-sm text-base-content/70">
{gettext("This page is not functional and only displays the planned features.")}
</span>
</div>
</div>
"""
end
# Status badge component
attr :status, :atom, required: true
defp status_badge(%{status: :paid} = assigns) do
~H"""
<span class="gap-1 badge badge-success">
<.icon name="hero-check-circle-mini" class="size-3" />
{gettext("Paid")}
</span>
"""
end
defp status_badge(%{status: :unpaid} = assigns) do
~H"""
<span class="gap-1 badge badge-error">
<.icon name="hero-x-circle-mini" class="size-3" />
{gettext("Unpaid")}
</span>
"""
end
defp status_badge(%{status: :suspended} = assigns) do
~H"""
<span class="gap-1 badge badge-neutral">
<.icon name="hero-pause-circle-mini" class="size-3" />
{gettext("Suspended")}
</span>
"""
end
defp period_row_class(:unpaid), do: "bg-error/5"
defp period_row_class(:suspended), do: "bg-base-200/50"
defp period_row_class(_), do: ""
# Mock member data
defp mock_member do
%{
id: "123",
first_name: "Maria",
last_name: "Weber",
email: "maria.weber@example.de",
contribution_type: gettext("Regular"),
joined_at: "15.03.2021",
contribution_start: "01.01.2021"
}
end
# Mock periods data
defp mock_periods do
[
%{
id: "p1",
period_start: "01.01.2025",
period_end: "31.12.2025",
interval: :yearly,
amount: Decimal.new("60.00"),
status: :unpaid,
notes: nil,
is_current: true
},
%{
id: "p2",
period_start: "01.01.2024",
period_end: "31.12.2024",
interval: :yearly,
amount: Decimal.new("60.00"),
status: :paid,
notes: gettext("Paid via bank transfer"),
is_current: false
},
%{
id: "p3",
period_start: "01.01.2023",
period_end: "31.12.2023",
interval: :yearly,
amount: Decimal.new("50.00"),
status: :paid,
notes: nil,
is_current: false
},
%{
id: "p4",
period_start: "01.01.2022",
period_end: "31.12.2022",
interval: :yearly,
amount: Decimal.new("50.00"),
status: :paid,
notes: nil,
is_current: false
},
%{
id: "p5",
period_start: "01.01.2021",
period_end: "31.12.2021",
interval: :yearly,
amount: Decimal.new("50.00"),
status: :suspended,
notes: gettext("Joining year - reduced to 0"),
is_current: false
}
]
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

View file

@ -0,0 +1,277 @@
defmodule MvWeb.ContributionSettingsLive do
@moduledoc """
Mock-up LiveView for Contribution Settings (Admin).
This is a preview-only page that displays the planned UI for managing
global contribution settings. It shows static mock data and is not functional.
## Planned Features (Future Implementation)
- Set default contribution type for new members
- Configure whether joining period is included in contributions
- Explanatory text with examples
## Settings
- `default_contribution_type_id` - UUID of the default contribution type
- `include_joining_period` - Boolean whether to include joining 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("Contribution Settings"))
|> assign(:contribution_types, mock_contribution_types())
|> assign(:selected_type_id, "1")
|> assign(:include_joining_period, true)}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.mockup_warning />
<.header>
{gettext("Contribution Settings")}
<:subtitle>
{gettext("Configure global settings for membership contributions.")}
</:subtitle>
</.header>
<div class="grid gap-6 lg:grid-cols-2">
<%!-- Settings Form --%>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">
<.icon name="hero-cog-6-tooth" class="size-5" />
{gettext("Global Settings")}
</h2>
<form class="space-y-6">
<%!-- Default Contribution Type --%>
<fieldset class="fieldset">
<label class="label">
<span class="label-text font-semibold">
{gettext("Default Contribution Type")}
</span>
</label>
<select class="select select-bordered w-full" disabled>
<option :for={ct <- @contribution_types} selected={ct.id == @selected_type_id}>
{ct.name} ({format_currency(ct.amount)}, {format_interval(ct.interval)})
</option>
</select>
<p class="text-sm text-base-content/60 mt-2">
{gettext(
"This contribution type is automatically assigned to all new members. Can be changed individually per member."
)}
</p>
</fieldset>
<%!-- Include Joining Period --%>
<fieldset class="fieldset">
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
class="checkbox checkbox-primary"
checked={@include_joining_period}
disabled
/>
<span class="label-text font-semibold">
{gettext("Include joining period")}
</span>
</label>
<div class="ml-9 space-y-2">
<p class="text-sm text-base-content/60">
{gettext("When active: Members pay from the period of their joining.")}
</p>
<p class="text-sm text-base-content/60">
{gettext("When inactive: Members pay from the next full period after joining.")}
</p>
</div>
</fieldset>
<div class="divider"></div>
<button type="button" class="btn btn-primary w-full" disabled>
<.icon name="hero-check" class="size-5" />
{gettext("Save Settings")}
</button>
</form>
</div>
</div>
<%!-- Examples Card --%>
<div class="card bg-base-200">
<div class="card-body">
<h2 class="card-title">
<.icon name="hero-light-bulb" class="size-5" />
{gettext("Examples")}
</h2>
<.example_section
title={gettext("Yearly Interval - Joining Period Included")}
joining_date="15.03.2023"
include_joining={true}
start_date="01.01.2023"
periods={["2023", "2024", "2025"]}
note={gettext("Member pays for the year they joined")}
/>
<div class="divider"></div>
<.example_section
title={gettext("Yearly Interval - Joining Period Excluded")}
joining_date="15.03.2023"
include_joining={false}
start_date="01.01.2024"
periods={["2024", "2025"]}
note={gettext("Member pays from the next full year")}
/>
<div class="divider"></div>
<.example_section
title={gettext("Quarterly Interval - Joining Period Excluded")}
joining_date="15.05.2024"
include_joining={false}
start_date="01.07.2024"
periods={["Q3/2024", "Q4/2024", "Q1/2025"]}
note={gettext("Member pays from the next full quarter")}
/>
<div class="divider"></div>
<.example_section
title={gettext("Monthly Interval - Joining Period Included")}
joining_date="15.03.2024"
include_joining={true}
start_date="01.03.2024"
periods={["03/2024", "04/2024", "05/2024", "..."]}
note={gettext("Member pays from the joining month")}
/>
</div>
</div>
</div>
<.example_member_card />
</Layouts.app>
"""
end
# Example member card with link to period view
defp example_member_card(assigns) do
~H"""
<div class="card bg-base-100 shadow-xl mt-6">
<div class="card-body">
<h2 class="card-title">
<.icon name="hero-user" class="size-5" />
{gettext("Example: Member Contribution View")}
</h2>
<p class="text-base-content/70">
{gettext(
"See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
)}
</p>
<div class="card-actions justify-end">
<.link navigate={~p"/contributions/member/example"} class="btn btn-primary btn-sm">
<.icon name="hero-eye" class="size-4" />
{gettext("View Example Member")}
</.link>
</div>
</div>
</div>
"""
end
# Mock-up warning banner component - subtle orange style
defp mockup_warning(assigns) do
~H"""
<div class="border border-warning text-warning bg-base-100 rounded-lg px-4 py-3 mb-6 flex items-center gap-3">
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
<div>
<span class="font-semibold">{gettext("Preview Mockup")}</span>
<span class="text-sm text-base-content/70 ml-2">
{gettext("This page is not functional and only displays the planned features.")}
</span>
</div>
</div>
"""
end
# Example section component
attr :title, :string, required: true
attr :joining_date, :string, required: true
attr :include_joining, :boolean, required: true
attr :start_date, :string, required: true
attr :periods, :list, required: true
attr :note, :string, required: true
defp example_section(assigns) do
~H"""
<div class="space-y-2">
<h3 class="font-semibold text-sm">{@title}</h3>
<div class="bg-base-300 rounded-lg p-3 text-sm space-y-1">
<p>
<span class="text-base-content/60">{gettext("Joining date")}:</span>
<span class="font-mono">{@joining_date}</span>
</p>
<p>
<span class="text-base-content/60">{gettext("Contribution start")}:</span>
<span class="font-mono font-semibold text-primary">{@start_date}</span>
</p>
<p>
<span class="text-base-content/60">{gettext("Generated periods")}:</span>
<span class="font-mono">
{Enum.join(@periods, ", ")}
</span>
</p>
</div>
<p class="text-xs text-base-content/60 italic"> {@note}</p>
</div>
"""
end
# Mock data for demonstration
defp mock_contribution_types do
[
%{
id: "1",
name: gettext("Regular"),
amount: Decimal.new("60.00"),
interval: :yearly
},
%{
id: "2",
name: gettext("Reduced"),
amount: Decimal.new("30.00"),
interval: :yearly
},
%{
id: "3",
name: gettext("Student"),
amount: Decimal.new("5.00"),
interval: :monthly
},
%{
id: "4",
name: gettext("Family"),
amount: Decimal.new("25.00"),
interval: :quarterly
}
]
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

View file

@ -0,0 +1,205 @@
defmodule MvWeb.ContributionTypeLive.Index do
@moduledoc """
Mock-up LiveView for Contribution Types Management (Admin).
This is a preview-only page that displays the planned UI for managing
contribution types. It shows static mock data and is not functional.
## Planned Features (Future Implementation)
- List all contribution types
- Display: Name, Amount, Interval, Member count
- Create new contribution types
- Edit existing contribution types (name, amount, description - NOT interval)
- Delete contribution types (if no members assigned)
## 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("Contribution Types"))
|> assign(:contribution_types, mock_contribution_types())}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.mockup_warning />
<.header>
{gettext("Contribution Types")}
<:subtitle>
{gettext("Manage contribution types for membership fees.")}
</:subtitle>
<:actions>
<button class="btn btn-primary" disabled>
<.icon name="hero-plus" /> {gettext("New Contribution Type")}
</button>
</:actions>
</.header>
<.table id="contribution_types" rows={@contribution_types} row_id={fn ct -> "ct-#{ct.id}" end}>
<:col :let={ct} label={gettext("Name")}>
<span class="font-medium">{ct.name}</span>
<p :if={ct.description} class="text-sm text-base-content/60">{ct.description}</p>
</:col>
<:col :let={ct} label={gettext("Amount")}>
<span class="font-mono">{format_currency(ct.amount)}</span>
</:col>
<:col :let={ct} label={gettext("Interval")}>
<span class="badge badge-outline">{format_interval(ct.interval)}</span>
</:col>
<:col :let={ct} label={gettext("Members")}>
<span class="badge badge-ghost">{ct.member_count}</span>
</:col>
<:action :let={_ct}>
<button class="btn btn-ghost btn-xs" disabled title={gettext("Edit")}>
<.icon name="hero-pencil" class="size-4" />
</button>
</:action>
<:action :let={ct}>
<button
class="btn btn-ghost btn-xs text-error"
disabled
title={
if ct.member_count > 0,
do: gettext("Cannot delete - members assigned"),
else: gettext("Delete")
}
>
<.icon name="hero-trash" class="size-4" />
</button>
</:action>
</.table>
<.info_card />
</Layouts.app>
"""
end
# Mock-up warning banner component - subtle orange style
defp mockup_warning(assigns) do
~H"""
<div class="border border-warning text-warning bg-base-100 rounded-lg px-4 py-3 mb-6 flex items-center gap-3">
<.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
<div>
<span class="font-semibold">{gettext("Preview Mockup")}</span>
<span class="text-sm text-base-content/70 ml-2">
{gettext("This page is not functional and only displays the planned features.")}
</span>
</div>
</div>
"""
end
# Info card explaining the contribution type concept
defp info_card(assigns) do
~H"""
<div class="card bg-base-200 mt-6">
<div class="card-body">
<h2 class="card-title">
<.icon name="hero-information-circle" class="size-5" />
{gettext("About Contribution Types")}
</h2>
<div class="prose prose-sm max-w-none">
<p>
{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."
)}
</p>
<ul>
<li>
<strong>{gettext("Name & Amount")}</strong>
- {gettext("Can be changed at any time. Amount changes affect future periods only.")}
</li>
<li>
<strong>{gettext("Interval")}</strong>
- {gettext(
"Fixed after creation. Members can only switch between types with the same interval."
)}
</li>
<li>
<strong>{gettext("Deletion")}</strong>
- {gettext("Only possible if no members are assigned to this type.")}
</li>
</ul>
</div>
</div>
</div>
"""
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

View file

@ -238,8 +238,8 @@ defmodule MvWeb.MemberLive.Form do
id -> Ash.get!(Mv.Membership.Member, id)
end
action = if is_nil(member), do: gettext("New"), else: gettext("Edit")
page_title = "#{action} #{gettext("Member")}"
page_title =
if is_nil(member), do: gettext("Create Member"), else: gettext("Edit Member")
{:ok,
socket

View file

@ -32,6 +32,7 @@ defmodule MvWeb.MemberLive.Index do
alias Mv.Membership
alias MvWeb.MemberLive.Index.Formatter
alias MvWeb.Helpers.DateFormatter
# Prefix used in sort field names for custom fields (e.g., "custom_field_<id>")
@custom_field_prefix "custom_field_"
@ -937,4 +938,7 @@ defmodule MvWeb.MemberLive.Index do
Map.get(visibility_config, Atom.to_string(field), true)
end)
end
# Public helper function to format dates for use in templates
def format_date(date), do: DateFormatter.format_date(date)
end

View file

@ -224,7 +224,7 @@
"""
}
>
{member.join_date}
{MvWeb.MemberLive.Index.format_date(member.join_date)}
</:col>
<:col :let={member} label={gettext("Paid")}>
<span class={[

View file

@ -6,6 +6,7 @@ defmodule MvWeb.MemberLive.Index.Formatter do
formats them appropriately for display in the UI.
"""
use Gettext, backend: MvWeb.Gettext
alias MvWeb.Helpers.DateFormatter
@doc """
Formats a custom field value for display.
@ -61,11 +62,11 @@ defmodule MvWeb.MemberLive.Index.Formatter do
defp format_value_by_type(value, :boolean, _) when value == false, do: gettext("No")
defp format_value_by_type(value, :boolean, _), do: to_string(value)
defp format_value_by_type(%Date{} = date, :date, _), do: Date.to_string(date)
defp format_value_by_type(%Date{} = date, :date, _), do: DateFormatter.format_date(date)
defp format_value_by_type(value, :date, _) when is_binary(value) do
case Date.from_iso8601(value) do
{:ok, date} -> Date.to_string(date)
{:ok, date} -> DateFormatter.format_date(date)
_ -> value
end
end

View file

@ -74,7 +74,7 @@ defmodule MvWeb.MemberLive.Show do
<div>
<.data_field label={gettext("Email")}>
<a
href={"mailto:#{format_email_mailto(@member.first_name, @member.last_name, @member.email)}"}
href={"mailto:#{MvWeb.MemberLive.Index.format_member_email(@member)}"}
class="text-blue-700 hover:text-blue-800 underline"
>
{@member.email}
@ -240,23 +240,10 @@ defmodule MvWeb.MemberLive.Show do
# Helper Functions
# -----------------------------------------------------------------
defp display_value(nil), do: ""
defp display_value(""), do: ""
defp display_value(nil), do: ""
defp display_value(""), do: ""
defp display_value(value), do: value
defp format_email_mailto(first_name, last_name, email) do
name =
[first_name, last_name]
|> Enum.filter(&(&1 && &1 != ""))
|> Enum.join(" ")
if name != "" do
"#{name} <#{email}>"
else
email
end
end
defp format_address(member) do
street_part =
[member.street, member.house_number]

View file

@ -42,7 +42,7 @@ defmodule MvWeb.UserLive.Form do
<:subtitle>{gettext("Use this form to manage user records in your database.")}</:subtitle>
</.header>
<.form for={@form} id="user-form" phx-change="validate" phx-submit="save">
<.form class="max-w-xl" for={@form} id="user-form" phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label={gettext("Email")} required type="email" />
<!-- Password Section -->
@ -61,7 +61,7 @@ defmodule MvWeb.UserLive.Form do
</label>
<%= if @show_password_fields do %>
<div class="mt-4 space-y-4 p-4 bg-gray-50 rounded-lg">
<div class="p-4 mt-4 space-y-4 rounded-lg bg-gray-50">
<.input
field={@form[:password]}
label={gettext("Password")}
@ -83,7 +83,7 @@ defmodule MvWeb.UserLive.Form do
<div class="text-sm text-gray-600">
<p><strong>{gettext("Password requirements")}:</strong></p>
<ul class="list-disc list-inside text-xs mt-1 space-y-1">
<ul class="mt-1 space-y-1 text-xs list-disc list-inside">
<li>{gettext("At least 8 characters")}</li>
<li>{gettext("Include both letters and numbers")}</li>
<li>{gettext("Consider using special characters")}</li>
@ -91,7 +91,7 @@ defmodule MvWeb.UserLive.Form do
</div>
<%= if @user do %>
<div class="mt-3 p-3 bg-orange-50 border border-orange-200 rounded">
<div class="p-3 mt-3 border border-orange-200 rounded bg-orange-50">
<p class="text-sm text-orange-800">
<strong>{gettext("Admin Note")}:</strong> {gettext(
"As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
@ -102,7 +102,7 @@ defmodule MvWeb.UserLive.Form do
</div>
<% else %>
<%= if @user do %>
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
<div class="p-4 mt-4 rounded-lg bg-blue-50">
<p class="text-sm text-blue-800">
<strong>{gettext("Note")}:</strong> {gettext(
"Check 'Change Password' above to set a new password for this user."
@ -110,7 +110,7 @@ defmodule MvWeb.UserLive.Form do
</p>
</div>
<% else %>
<div class="mt-4 p-4 bg-yellow-50 rounded-lg">
<div class="p-4 mt-4 rounded-lg bg-yellow-50">
<p class="text-sm text-yellow-800">
<strong>{gettext("Note")}:</strong> {gettext(
"User will be created without a password. Check 'Set Password' to add one."
@ -123,11 +123,11 @@ defmodule MvWeb.UserLive.Form do
<!-- Member Linking Section -->
<div class="mt-6">
<h2 class="text-base font-semibold mb-3">{gettext("Linked Member")}</h2>
<h2 class="mb-3 text-base font-semibold">{gettext("Linked Member")}</h2>
<%= if @user && @user.member && !@unlink_member do %>
<!-- Show linked member with unlink button -->
<div class="p-4 bg-green-50 border border-green-200 rounded-lg">
<div class="p-4 border border-green-200 rounded-lg bg-green-50">
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-green-900">
@ -147,7 +147,7 @@ defmodule MvWeb.UserLive.Form do
<% else %>
<%= if @unlink_member do %>
<!-- Show unlink pending message -->
<div class="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div class="p-4 border border-yellow-200 rounded-lg bg-yellow-50">
<p class="text-sm text-yellow-800">
<strong>{gettext("Unlinking scheduled")}:</strong> {gettext(
"Member will be unlinked when you save. Cannot select new member until saved."
@ -219,7 +219,7 @@ defmodule MvWeb.UserLive.Form do
</div>
<%= if @user && @user.email && @available_members != [] && Enum.all?(@available_members, &(&1.email == to_string(@user.email))) do %>
<div class="p-3 bg-yellow-50 border border-yellow-200 rounded">
<div class="p-3 border border-yellow-200 rounded bg-yellow-50">
<p class="text-sm text-yellow-800">
<strong>{gettext("Note")}:</strong> {gettext(
"A member with this email already exists. To link with a different member, please change one of the email addresses first."
@ -231,12 +231,12 @@ defmodule MvWeb.UserLive.Form do
<%= if @selected_member_id && @selected_member_name do %>
<div
id="member-selected"
class="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg"
class="p-3 mt-2 border border-blue-200 rounded-lg bg-blue-50"
>
<p class="text-sm text-blue-800">
<strong>{gettext("Selected")}:</strong> {@selected_member_name}
</p>
<p class="text-xs text-blue-600 mt-1">
<p class="mt-1 text-xs text-blue-600">
{gettext("Save to confirm linking.")}
</p>
</div>
@ -245,10 +245,12 @@ defmodule MvWeb.UserLive.Form do
<% end %>
</div>
<div class="mt-4">
<.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save User")}
</.button>
<.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")}</.button>
</div>
</.form>
</Layouts.app>
"""

View file

@ -49,7 +49,6 @@
>
{user.email}
</:col>
<:col :let={user} label={gettext("OIDC ID")}>{user.oidc_id}</:col>
<:col :let={user} label={gettext("Linked Member")}>
<%= if user.member do %>
{user.member.first_name} {user.member.last_name}

View file

@ -46,9 +46,7 @@ defmodule MvWeb.UserLive.Show do
</.header>
<.list>
<:item title={gettext("ID")}>{@user.id}</:item>
<:item title={gettext("Email")}>{@user.email}</:item>
<:item title={gettext("OIDC ID")}>{@user.oidc_id || gettext("Not set")}</:item>
<:item title={gettext("Password Authentication")}>
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
</:item>
@ -56,13 +54,13 @@ defmodule MvWeb.UserLive.Show do
<%= if @user.member do %>
<.link
navigate={~p"/members/#{@user.member}"}
class="text-blue-600 hover:text-blue-800 underline"
class="text-blue-600 underline hover:text-blue-800"
>
<.icon name="hero-users" class="h-4 w-4 inline mr-1" />
<.icon name="hero-users" class="inline w-4 h-4 mr-1" />
{@user.member.first_name} {@user.member.last_name}
</.link>
<% else %>
<span class="text-gray-500 italic">{gettext("No member linked")}</span>
<span class="italic text-gray-500">{gettext("No member linked")}</span>
<% end %>
</:item>
</.list>

View file

@ -75,6 +75,11 @@ defmodule MvWeb.Router do
live "/settings", GlobalSettingsLive
# Contribution Management (Mock-ups)
live "/contribution_types", ContributionTypeLive.Index, :index
live "/contribution_settings", ContributionSettingsLive
live "/contributions/member/:id", ContributionPeriodLive.Show, :show
post "/set_locale", LocaleController, :set_locale
end

View file

@ -10,13 +10,14 @@ msgid ""
msgstr ""
"Language: en\n"
#: lib/mv_web/components/core_components.ex:386
#: lib/mv_web/components/core_components.ex:387
#: lib/mv_web/live/contribution_period_live/show.ex:141
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr "Aktionen"
#: lib/mv_web/live/member_live/index.html.heex:248
#: lib/mv_web/live/user_live/index.html.heex:72
#: lib/mv_web/live/user_live/index.html.heex:71
#, elixir-autogen, elixir-format
msgid "Are you sure?"
msgstr "Bist du sicher?"
@ -33,32 +34,35 @@ msgstr "Verbindung wird wiederhergestellt"
msgid "City"
msgstr "Stadt"
#: lib/mv_web/live/contribution_type_live/index.ex:78
#: lib/mv_web/live/member_live/index.html.heex:250
#: lib/mv_web/live/user_live/index.html.heex:74
#: lib/mv_web/live/user_live/index.html.heex:73
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr "Löschen"
#: lib/mv_web/live/member_live/form.ex:241
#: lib/mv_web/live/contribution_type_live/index.ex:66
#: lib/mv_web/live/member_live/index.html.heex:242
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/index.html.heex:66
#: lib/mv_web/live/user_live/form.ex:267
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr "Bearbeite"
#: lib/mv_web/live/member_live/form.ex:242
#: lib/mv_web/live/member_live/show.ex:40
#: lib/mv_web/live/member_live/show.ex:197
#, elixir-autogen, elixir-format
msgid "Edit Member"
msgstr "Mitglied bearbeiten"
#: lib/mv_web/live/contribution_period_live/show.ex:58
#: lib/mv_web/live/member_live/form.ex:106
#: lib/mv_web/live/member_live/index.html.heex:112
#: lib/mv_web/live/member_live/show.ex:75
#: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44
#: lib/mv_web/live/user_live/show.ex:50
#: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "Email"
msgstr "E-Mail"
@ -89,7 +93,7 @@ msgid "New Member"
msgstr "Neues Mitglied"
#: lib/mv_web/live/member_live/index.html.heex:239
#: lib/mv_web/live/user_live/index.html.heex:63
#: lib/mv_web/live/user_live/index.html.heex:62
#, elixir-autogen, elixir-format
msgid "Show"
msgstr "Anzeigen"
@ -120,6 +124,7 @@ msgstr "Austrittsdatum"
msgid "House Number"
msgstr "Hausnummer"
#: lib/mv_web/live/contribution_period_live/show.ex:140
#: lib/mv_web/live/member_live/form.ex:126
#: lib/mv_web/live/member_live/show.ex:124
#, elixir-autogen, elixir-format
@ -128,6 +133,8 @@ msgstr "Notizen"
#: lib/mv_web/live/components/payment_filter_component.ex:94
#: lib/mv_web/live/components/payment_filter_component.ex:144
#: lib/mv_web/live/contribution_period_live/show.ex:186
#: lib/mv_web/live/contribution_period_live/show.ex:241
#: lib/mv_web/live/member_live/form.ex:199
#: lib/mv_web/live/member_live/index.html.heex:229
#: lib/mv_web/live/member_live/show.ex:162
@ -157,7 +164,7 @@ msgstr "Mitglied speichern"
#: lib/mv_web/live/global_settings_live.ex:55
#: lib/mv_web/live/member_live/form.ex:48
#: lib/mv_web/live/member_live/form.ex:210
#: lib/mv_web/live/user_live/form.ex:248
#: lib/mv_web/live/user_live/form.ex:249
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr "Speichern..."
@ -169,8 +176,8 @@ msgid "Street"
msgstr "Straße"
#: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:61
#: lib/mv_web/live/member_live/show.ex:303
#: lib/mv_web/live/member_live/index/formatter.ex:62
#: lib/mv_web/live/member_live/show.ex:290
#, elixir-autogen, elixir-format
msgid "No"
msgstr "Nein"
@ -181,8 +188,8 @@ msgid "Show Member"
msgstr "Mitglied anzeigen"
#: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:60
#: lib/mv_web/live/member_live/show.ex:303
#: lib/mv_web/live/member_live/index/formatter.ex:61
#: lib/mv_web/live/member_live/show.ex:290
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr "Ja"
@ -240,7 +247,7 @@ msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
#: lib/mv_web/live/custom_field_live/index.ex:120
#: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/member_live/form.ex:208
#: lib/mv_web/live/user_live/form.ex:251
#: lib/mv_web/live/user_live/form.ex:252
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr "Abbrechen"
@ -260,22 +267,17 @@ msgstr "Beschreibung"
msgid "Edit User"
msgstr "Benutzer*in bearbeiten"
#: lib/mv_web/live/user_live/show.ex:53
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Enabled"
msgstr "Aktiviert"
#: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "ID"
msgstr "ID"
#: lib/mv_web/live/custom_field_live/form.ex:62
#, elixir-autogen, elixir-format
msgid "Immutable"
msgstr "Unveränderlich"
#: lib/mv_web/components/layouts/navbar.ex:102
#: lib/mv_web/components/layouts/navbar.ex:113
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr "Abmelden"
@ -287,18 +289,19 @@ msgid "Listing Users"
msgstr "Benutzer*innen auflisten"
#: lib/mv_web/live/custom_field_value_live/form.ex:60
#: lib/mv_web/live/member_live/form.ex:242
#, elixir-autogen, elixir-format
msgid "Member"
msgstr "Mitglied"
#: lib/mv_web/components/layouts/navbar.ex:25
#: lib/mv_web/live/member_live/index.ex:73
#: lib/mv_web/live/contribution_type_live/index.ex:61
#: lib/mv_web/live/member_live/index.ex:74
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr "Mitglieder"
#: lib/mv_web/live/contribution_type_live/index.ex:48
#: lib/mv_web/live/custom_field_live/form.ex:51
#, elixir-autogen, elixir-format
msgid "Name"
@ -309,16 +312,12 @@ msgstr "Name"
msgid "New User"
msgstr "Neue*r Benutzer*in"
#: lib/mv_web/live/user_live/show.ex:53
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Not enabled"
msgstr "Nicht aktiviert"
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Not set"
msgstr "Nicht gesetzt"
#: lib/mv_web/live/contribution_period_live/show.ex:207
#: lib/mv_web/live/user_live/form.ex:107
#: lib/mv_web/live/user_live/form.ex:115
#: lib/mv_web/live/user_live/form.ex:224
@ -326,18 +325,12 @@ msgstr "Nicht gesetzt"
msgid "Note"
msgstr "Hinweis"
#: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "OIDC ID"
msgstr "OIDC ID"
#: lib/mv_web/live/user_live/show.ex:52
#: lib/mv_web/live/user_live/show.ex:50
#, elixir-autogen, elixir-format
msgid "Password Authentication"
msgstr "Passwort-Authentifizierung"
#: lib/mv_web/components/layouts/navbar.ex:95
#: lib/mv_web/components/layouts/navbar.ex:106
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr "Profil"
@ -357,17 +350,17 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member"
msgstr "Mitglied auswählen"
#: lib/mv_web/components/layouts/navbar.ex:99
#: lib/mv_web/components/layouts/navbar.ex:110
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr "Einstellungen"
#: lib/mv_web/live/user_live/form.ex:249
#: lib/mv_web/live/user_live/form.ex:250
#, elixir-autogen, elixir-format
msgid "Save User"
msgstr "Benutzer*in speichern"
#: lib/mv_web/live/user_live/show.ex:79
#: lib/mv_web/live/user_live/show.ex:77
#, elixir-autogen, elixir-format
msgid "Show User"
msgstr "Benutzer*in anzeigen"
@ -387,7 +380,7 @@ msgstr "Nicht unterstützter Wertetyp: %{type}"
msgid "Use this form to manage user records in your database."
msgstr "Verwenden Sie dieses Formular, um Benutzer*innen-Datensätze zu verwalten."
#: lib/mv_web/live/user_live/form.ex:266
#: lib/mv_web/live/user_live/form.ex:268
#: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "User"
@ -415,8 +408,7 @@ msgstr "aufsteigend"
msgid "descending"
msgstr "absteigend"
#: lib/mv_web/live/member_live/form.ex:241
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/form.ex:267
#, elixir-autogen, elixir-format
msgid "New"
msgstr "Neue*r"
@ -492,8 +484,8 @@ msgid "User will be created without a password. Check 'Set Password' to add one.
msgstr "Benutzer*in wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
#: lib/mv_web/live/user_live/form.ex:126
#: lib/mv_web/live/user_live/index.html.heex:53
#: lib/mv_web/live/user_live/show.ex:55
#: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Linked Member"
msgstr "Verknüpftes Mitglied"
@ -503,8 +495,8 @@ msgstr "Verknüpftes Mitglied"
msgid "Linked User"
msgstr "Verknüpfte*r Benutzer*in"
#: lib/mv_web/live/user_live/index.html.heex:57
#: lib/mv_web/live/user_live/show.ex:65
#: lib/mv_web/live/user_live/index.html.heex:56
#: lib/mv_web/live/user_live/show.ex:63
#, elixir-autogen, elixir-format
msgid "No member linked"
msgstr "Kein Mitglied verknüpft"
@ -525,14 +517,14 @@ msgstr "Zurück zur Mitgliederliste"
msgid "Back to users list"
msgstr "Zurück zur Benutzer*innen-Liste"
#: lib/mv_web/components/layouts/navbar.ex:33
#: lib/mv_web/components/layouts/navbar.ex:39
#: lib/mv_web/components/layouts/navbar.ex:44
#: lib/mv_web/components/layouts/navbar.ex:50
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr "Sprache auswählen"
#: lib/mv_web/components/layouts/navbar.ex:46
#: lib/mv_web/components/layouts/navbar.ex:66
#: lib/mv_web/components/layouts/navbar.ex:57
#: lib/mv_web/components/layouts/navbar.ex:77
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr "Dunklen Modus umschalten"
@ -700,6 +692,7 @@ msgstr "Vereinsdaten"
msgid "Manage global settings for the association."
msgstr "Passe übergreifende Einstellungen für den Verein an."
#: lib/mv_web/live/contribution_settings_live.ex:102
#: lib/mv_web/live/global_settings_live.ex:56
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Settings"
@ -720,7 +713,7 @@ msgstr "Ein Mitglied mit dieser E-Mail-Adresse existiert bereits. Um mit einem a
msgid "Available members"
msgstr "Verfügbare Mitglieder"
#: lib/mv_web/live/user_live/form.ex:357
#: lib/mv_web/live/user_live/form.ex:359
#, elixir-autogen, elixir-format
msgid "Failed to link member: %{error}"
msgstr "Fehler beim Verlinken des Mitglieds: %{error}"
@ -760,7 +753,7 @@ msgstr "Mitglied entverknüpfen"
msgid "Unlinking scheduled"
msgstr "Entverknüpfung geplant"
#: lib/mv_web/live/member_live/index.ex:159
#: lib/mv_web/live/member_live/index.ex:160
#, elixir-autogen, elixir-format
msgid "Copied %{count} email address to clipboard"
msgid_plural "Copied %{count} email addresses to clipboard"
@ -777,12 +770,12 @@ msgstr "E-Mail-Adressen der ausgewählten Mitglieder kopieren"
msgid "Copy emails"
msgstr "E-Mails kopieren"
#: lib/mv_web/live/member_live/index.ex:148
#: lib/mv_web/live/member_live/index.ex:149
#, elixir-autogen, elixir-format
msgid "No email addresses found"
msgstr "Keine E-Mail-Adressen gefunden"
#: lib/mv_web/live/member_live/index.ex:145
#: lib/mv_web/live/member_live/index.ex:146
#, elixir-autogen, elixir-format
msgid "No members selected"
msgstr "Keine Mitglieder ausgewählt"
@ -797,7 +790,7 @@ msgstr "E-Mail-Programm mit BCC-Empfänger*innen öffnen"
msgid "Open in email program"
msgstr "Im E-Mail-Programm öffnen"
#: lib/mv_web/live/member_live/index.ex:168
#: lib/mv_web/live/member_live/index.ex:169
#, elixir-autogen, elixir-format
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
msgstr "Tipp: E-Mail-Adressen ins BCC-Feld einfügen für Datenschutzkonformität"
@ -923,11 +916,458 @@ msgstr "monatlich"
msgid "yearly"
msgstr "jährlich"
#: lib/mv_web/live/member_live/form.ex:242
#, elixir-autogen, elixir-format
msgid "Create Member"
msgstr "Mitglied erstellen"
#: lib/mv_web/live/contribution_period_live/show.ex:107
#, elixir-autogen, elixir-format
msgid "%{count} period selected"
msgid_plural "%{count} periods selected"
msgstr[0] ""
msgstr[1] ""
#: lib/mv_web/live/contribution_type_live/index.ex:113
#, elixir-autogen, elixir-format
msgid "About Contribution Types"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:138
#: lib/mv_web/live/contribution_type_live/index.ex:53
#, elixir-autogen, elixir-format
msgid "Amount"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:48
#, elixir-autogen, elixir-format
msgid "Back to Settings"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:124
#, elixir-autogen, elixir-format
msgid "Can be changed at any time. Amount changes affect future periods only."
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:77
#, elixir-autogen, elixir-format
msgid "Cannot delete - members assigned"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:83
#, elixir-autogen, elixir-format
msgid "Change Contribution Type"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:42
#, elixir-autogen, elixir-format
msgid "Configure global settings for membership contributions."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:34
#: lib/mv_web/live/contribution_settings_live.ex:27
#: lib/mv_web/live/contribution_settings_live.ex:40
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Settings"
msgstr "Beitrag"
#: lib/mv_web/live/contribution_period_live/show.ex:62
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Start"
msgstr "Beitrag"
#: lib/mv_web/components/layouts/navbar.ex:32
#: lib/mv_web/live/contribution_type_live/index.ex:25
#: lib/mv_web/live/contribution_type_live/index.ex:36
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Types"
msgstr "Beitrag"
#: lib/mv_web/live/contribution_settings_live.ex:224
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution start"
msgstr "Beitrag"
#: lib/mv_web/live/contribution_period_live/show.ex:41
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution type"
msgstr "Beitrag"
#: lib/mv_web/live/contribution_type_live/index.ex:117
#, elixir-autogen, elixir-format
msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:30
#, elixir-autogen, elixir-format, fuzzy
msgid "Contributions"
msgstr "Beitrag"
#: lib/mv_web/live/contribution_period_live/show.ex:39
#, elixir-autogen, elixir-format, fuzzy
msgid "Contributions for %{name}"
msgstr "Beitrag"
#: lib/mv_web/live/contribution_period_live/show.ex:159
#, elixir-autogen, elixir-format
msgid "Current"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:60
#, elixir-autogen, elixir-format
msgid "Default Contribution Type"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:133
#, elixir-autogen, elixir-format, fuzzy
msgid "Deletion"
msgstr "Löschen"
#: lib/mv_web/live/contribution_settings_live.ex:173
#, elixir-autogen, elixir-format
msgid "Example: Member Contribution View"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:113
#, elixir-autogen, elixir-format
msgid "Examples"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:262
#: lib/mv_web/live/contribution_type_live/index.ex:172
#, elixir-autogen, elixir-format
msgid "Family"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:128
#, elixir-autogen, elixir-format
msgid "Fixed after creation. Members can only switch between types with the same interval."
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:228
#, elixir-autogen, elixir-format
msgid "Generated periods"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:52
#, elixir-autogen, elixir-format, fuzzy
msgid "Global Settings"
msgstr "Vereinsdaten"
#: lib/mv_web/live/contribution_period_live/show.ex:343
#: lib/mv_web/live/contribution_settings_live.ex:275
#: lib/mv_web/live/contribution_type_live/index.ex:203
#, elixir-autogen, elixir-format
msgid "Half-yearly"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:181
#, elixir-autogen, elixir-format
msgid "Half-yearly contribution for supporting members"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:87
#: lib/mv_web/live/contribution_type_live/index.ex:188
#, elixir-autogen, elixir-format
msgid "Honorary"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:85
#, elixir-autogen, elixir-format
msgid "Include joining period"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:137
#: lib/mv_web/live/contribution_type_live/index.ex:57
#: lib/mv_web/live/contribution_type_live/index.ex:127
#, elixir-autogen, elixir-format
msgid "Interval"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:220
#, elixir-autogen, elixir-format, fuzzy
msgid "Joining date"
msgstr "Beitrittsdatum"
#: lib/mv_web/live/contribution_period_live/show.ex:331
#, elixir-autogen, elixir-format
msgid "Joining year - reduced to 0"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:38
#, elixir-autogen, elixir-format
msgid "Manage contribution types for membership fees."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:116
#, elixir-autogen, elixir-format
msgid "Mark as Paid"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:120
#, elixir-autogen, elixir-format
msgid "Mark as Suspended"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:124
#, elixir-autogen, elixir-format
msgid "Mark as Unpaid"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "Member Contributions"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:122
#, elixir-autogen, elixir-format
msgid "Member pays for the year they joined"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:155
#, elixir-autogen, elixir-format
msgid "Member pays from the joining month"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:144
#, elixir-autogen, elixir-format
msgid "Member pays from the next full quarter"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:133
#, elixir-autogen, elixir-format
msgid "Member pays from the next full year"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:43
#, elixir-autogen, elixir-format, fuzzy
msgid "Member since"
msgstr "Mitglieder"
#: lib/mv_web/live/contribution_period_live/show.ex:92
#, elixir-autogen, elixir-format
msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:341
#: lib/mv_web/live/contribution_settings_live.ex:273
#: lib/mv_web/live/contribution_type_live/index.ex:201
#, elixir-autogen, elixir-format, fuzzy
msgid "Monthly"
msgstr "monatlich"
#: lib/mv_web/live/contribution_settings_live.ex:150
#, elixir-autogen, elixir-format
msgid "Monthly Interval - Joining Period Included"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:165
#, elixir-autogen, elixir-format
msgid "Monthly fee for students and trainees"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:123
#, elixir-autogen, elixir-format
msgid "Name & Amount"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:42
#, elixir-autogen, elixir-format, fuzzy
msgid "New Contribution Type"
msgstr "Beitrag"
#: lib/mv_web/live/contribution_type_live/index.ex:189
#, elixir-autogen, elixir-format
msgid "No fee for honorary members"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:134
#, elixir-autogen, elixir-format
msgid "Only possible if no members are assigned to this type."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:70
#, elixir-autogen, elixir-format
msgid "Open Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:301
#, elixir-autogen, elixir-format
msgid "Paid via bank transfer"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:225
#: lib/mv_web/live/contribution_settings_live.ex:197
#: lib/mv_web/live/contribution_type_live/index.ex:97
#, elixir-autogen, elixir-format
msgid "Preview Mockup"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:342
#: lib/mv_web/live/contribution_settings_live.ex:274
#: lib/mv_web/live/contribution_type_live/index.ex:202
#, elixir-autogen, elixir-format
msgid "Quarterly"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:139
#, elixir-autogen, elixir-format
msgid "Quarterly Interval - Joining Period Excluded"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:173
#, elixir-autogen, elixir-format
msgid "Quarterly fee for family memberships"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:86
#: lib/mv_web/live/contribution_settings_live.ex:250
#: lib/mv_web/live/contribution_type_live/index.ex:156
#, elixir-autogen, elixir-format
msgid "Reduced"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:157
#, elixir-autogen, elixir-format
msgid "Reduced fee for unemployed, pensioners, or low income"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:275
#: lib/mv_web/live/contribution_settings_live.ex:244
#: lib/mv_web/live/contribution_type_live/index.ex:148
#, elixir-autogen, elixir-format
msgid "Regular"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:204
#, elixir-autogen, elixir-format
msgid "Reopen"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:176
#, elixir-autogen, elixir-format
msgid "See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:149
#, elixir-autogen, elixir-format
msgid "Standard membership fee for regular members"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:139
#, elixir-autogen, elixir-format
msgid "Status"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:256
#: lib/mv_web/live/contribution_type_live/index.ex:164
#, elixir-autogen, elixir-format
msgid "Student"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:180
#, elixir-autogen, elixir-format
msgid "Supporting Member"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:195
#, elixir-autogen, elixir-format
msgid "Suspend"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:259
#, elixir-autogen, elixir-format
msgid "Suspended"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:69
#, elixir-autogen, elixir-format
msgid "This contribution type is automatically assigned to all new members. Can be changed individually per member."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:227
#: lib/mv_web/live/contribution_settings_live.ex:199
#: lib/mv_web/live/contribution_type_live/index.ex:99
#, elixir-autogen, elixir-format
msgid "This page is not functional and only displays the planned features."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:136
#, elixir-autogen, elixir-format
msgid "Time Period"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:66
#, elixir-autogen, elixir-format
msgid "Total Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:250
#, elixir-autogen, elixir-format
msgid "Unpaid"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:183
#, elixir-autogen, elixir-format
msgid "View Example Member"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:90
#, elixir-autogen, elixir-format
msgid "When active: Members pay from the period of their joining."
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:93
#, elixir-autogen, elixir-format
msgid "When inactive: Members pay from the next full period after joining."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:98
#, elixir-autogen, elixir-format
msgid "Why are not all contribution types shown?"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:85
#: lib/mv_web/live/contribution_period_live/show.ex:86
#: lib/mv_web/live/contribution_period_live/show.ex:87
#: lib/mv_web/live/contribution_period_live/show.ex:344
#: lib/mv_web/live/contribution_settings_live.ex:276
#: lib/mv_web/live/contribution_type_live/index.ex:204
#, elixir-autogen, elixir-format, fuzzy
msgid "Yearly"
msgstr "jährlich"
#: lib/mv_web/live/contribution_settings_live.ex:128
#, elixir-autogen, elixir-format
msgid "Yearly Interval - Joining Period Excluded"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:117
#, elixir-autogen, elixir-format
msgid "Yearly Interval - Joining Period Included"
msgstr ""
#~ #: lib/mv_web/live/user_live/show.ex:49
#~ #, elixir-autogen, elixir-format
#~ msgid "ID"
#~ msgstr "ID"
#~ #: lib/mv_web/live/member_live/show.ex:47
#~ #, elixir-autogen, elixir-format
#~ msgid "Id"
#~ msgstr "ID"
#~ #: lib/mv_web/live/user_live/show.ex:51
#~ #, elixir-autogen, elixir-format
#~ msgid "Not set"
#~ msgstr "Nicht gesetzt"
#~ #: lib/mv_web/live/user_live/index.html.heex:52
#~ #: lib/mv_web/live/user_live/show.ex:51
#~ #, elixir-autogen, elixir-format
#~ msgid "OIDC ID"
#~ msgstr "OIDC ID"
#~ #: lib/mv_web/live/member_live/show.ex:33
#~ #, elixir-autogen, elixir-format
#~ msgid "This is a member record from your database."

View file

@ -11,13 +11,14 @@
msgid ""
msgstr ""
#: lib/mv_web/components/core_components.ex:386
#: lib/mv_web/components/core_components.ex:387
#: lib/mv_web/live/contribution_period_live/show.ex:141
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:248
#: lib/mv_web/live/user_live/index.html.heex:72
#: lib/mv_web/live/user_live/index.html.heex:71
#, elixir-autogen, elixir-format
msgid "Are you sure?"
msgstr ""
@ -34,32 +35,35 @@ msgstr ""
msgid "City"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:78
#: lib/mv_web/live/member_live/index.html.heex:250
#: lib/mv_web/live/user_live/index.html.heex:74
#: lib/mv_web/live/user_live/index.html.heex:73
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:241
#: lib/mv_web/live/contribution_type_live/index.ex:66
#: lib/mv_web/live/member_live/index.html.heex:242
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/index.html.heex:66
#: lib/mv_web/live/user_live/form.ex:267
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:242
#: lib/mv_web/live/member_live/show.ex:40
#: lib/mv_web/live/member_live/show.ex:197
#, elixir-autogen, elixir-format
msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:58
#: lib/mv_web/live/member_live/form.ex:106
#: lib/mv_web/live/member_live/index.html.heex:112
#: lib/mv_web/live/member_live/show.ex:75
#: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44
#: lib/mv_web/live/user_live/show.ex:50
#: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
@ -90,7 +94,7 @@ msgid "New Member"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:239
#: lib/mv_web/live/user_live/index.html.heex:63
#: lib/mv_web/live/user_live/index.html.heex:62
#, elixir-autogen, elixir-format
msgid "Show"
msgstr ""
@ -121,6 +125,7 @@ msgstr ""
msgid "House Number"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:140
#: lib/mv_web/live/member_live/form.ex:126
#: lib/mv_web/live/member_live/show.ex:124
#, elixir-autogen, elixir-format
@ -129,6 +134,8 @@ msgstr ""
#: lib/mv_web/live/components/payment_filter_component.ex:94
#: lib/mv_web/live/components/payment_filter_component.ex:144
#: lib/mv_web/live/contribution_period_live/show.ex:186
#: lib/mv_web/live/contribution_period_live/show.ex:241
#: lib/mv_web/live/member_live/form.ex:199
#: lib/mv_web/live/member_live/index.html.heex:229
#: lib/mv_web/live/member_live/show.ex:162
@ -158,7 +165,7 @@ msgstr ""
#: lib/mv_web/live/global_settings_live.ex:55
#: lib/mv_web/live/member_live/form.ex:48
#: lib/mv_web/live/member_live/form.ex:210
#: lib/mv_web/live/user_live/form.ex:248
#: lib/mv_web/live/user_live/form.ex:249
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
@ -170,8 +177,8 @@ msgid "Street"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:61
#: lib/mv_web/live/member_live/show.ex:303
#: lib/mv_web/live/member_live/index/formatter.ex:62
#: lib/mv_web/live/member_live/show.ex:290
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
@ -182,8 +189,8 @@ msgid "Show Member"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:60
#: lib/mv_web/live/member_live/show.ex:303
#: lib/mv_web/live/member_live/index/formatter.ex:61
#: lib/mv_web/live/member_live/show.ex:290
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr ""
@ -241,7 +248,7 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:120
#: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/member_live/form.ex:208
#: lib/mv_web/live/user_live/form.ex:251
#: lib/mv_web/live/user_live/form.ex:252
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
@ -261,22 +268,17 @@ msgstr ""
msgid "Edit User"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:53
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Enabled"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "ID"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:62
#, elixir-autogen, elixir-format
msgid "Immutable"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:102
#: lib/mv_web/components/layouts/navbar.ex:113
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr ""
@ -288,18 +290,19 @@ msgid "Listing Users"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:60
#: lib/mv_web/live/member_live/form.ex:242
#, elixir-autogen, elixir-format
msgid "Member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:25
#: lib/mv_web/live/member_live/index.ex:73
#: lib/mv_web/live/contribution_type_live/index.ex:61
#: lib/mv_web/live/member_live/index.ex:74
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:48
#: lib/mv_web/live/custom_field_live/form.ex:51
#, elixir-autogen, elixir-format
msgid "Name"
@ -310,16 +313,12 @@ msgstr ""
msgid "New User"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:53
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Not enabled"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Not set"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:207
#: lib/mv_web/live/user_live/form.ex:107
#: lib/mv_web/live/user_live/form.ex:115
#: lib/mv_web/live/user_live/form.ex:224
@ -327,18 +326,12 @@ msgstr ""
msgid "Note"
msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "OIDC ID"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:52
#: lib/mv_web/live/user_live/show.ex:50
#, elixir-autogen, elixir-format
msgid "Password Authentication"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:95
#: lib/mv_web/components/layouts/navbar.ex:106
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
@ -358,17 +351,17 @@ msgstr ""
msgid "Select member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:99
#: lib/mv_web/components/layouts/navbar.ex:110
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:249
#: lib/mv_web/live/user_live/form.ex:250
#, elixir-autogen, elixir-format
msgid "Save User"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:79
#: lib/mv_web/live/user_live/show.ex:77
#, elixir-autogen, elixir-format
msgid "Show User"
msgstr ""
@ -388,7 +381,7 @@ msgstr ""
msgid "Use this form to manage user records in your database."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:266
#: lib/mv_web/live/user_live/form.ex:268
#: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "User"
@ -416,8 +409,7 @@ msgstr ""
msgid "descending"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:241
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/form.ex:267
#, elixir-autogen, elixir-format
msgid "New"
msgstr ""
@ -493,8 +485,8 @@ msgid "User will be created without a password. Check 'Set Password' to add one.
msgstr ""
#: lib/mv_web/live/user_live/form.ex:126
#: lib/mv_web/live/user_live/index.html.heex:53
#: lib/mv_web/live/user_live/show.ex:55
#: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format
msgid "Linked Member"
msgstr ""
@ -504,8 +496,8 @@ msgstr ""
msgid "Linked User"
msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:57
#: lib/mv_web/live/user_live/show.ex:65
#: lib/mv_web/live/user_live/index.html.heex:56
#: lib/mv_web/live/user_live/show.ex:63
#, elixir-autogen, elixir-format
msgid "No member linked"
msgstr ""
@ -526,14 +518,14 @@ msgstr ""
msgid "Back to users list"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:33
#: lib/mv_web/components/layouts/navbar.ex:39
#: lib/mv_web/components/layouts/navbar.ex:44
#: lib/mv_web/components/layouts/navbar.ex:50
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:46
#: lib/mv_web/components/layouts/navbar.ex:66
#: lib/mv_web/components/layouts/navbar.ex:57
#: lib/mv_web/components/layouts/navbar.ex:77
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr ""
@ -701,6 +693,7 @@ msgstr ""
msgid "Manage global settings for the association."
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:102
#: lib/mv_web/live/global_settings_live.ex:56
#, elixir-autogen, elixir-format
msgid "Save Settings"
@ -721,7 +714,7 @@ msgstr ""
msgid "Available members"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:357
#: lib/mv_web/live/user_live/form.ex:359
#, elixir-autogen, elixir-format
msgid "Failed to link member: %{error}"
msgstr ""
@ -761,7 +754,7 @@ msgstr ""
msgid "Unlinking scheduled"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:159
#: lib/mv_web/live/member_live/index.ex:160
#, elixir-autogen, elixir-format
msgid "Copied %{count} email address to clipboard"
msgid_plural "Copied %{count} email addresses to clipboard"
@ -778,12 +771,12 @@ msgstr ""
msgid "Copy emails"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:148
#: lib/mv_web/live/member_live/index.ex:149
#, elixir-autogen, elixir-format
msgid "No email addresses found"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:145
#: lib/mv_web/live/member_live/index.ex:146
#, elixir-autogen, elixir-format
msgid "No members selected"
msgstr ""
@ -798,7 +791,7 @@ msgstr ""
msgid "Open in email program"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:168
#: lib/mv_web/live/member_live/index.ex:169
#, elixir-autogen, elixir-format
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
msgstr ""
@ -923,3 +916,434 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "yearly"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:242
#, elixir-autogen, elixir-format
msgid "Create Member"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:107
#, elixir-autogen, elixir-format
msgid "%{count} period selected"
msgid_plural "%{count} periods selected"
msgstr[0] ""
msgstr[1] ""
#: lib/mv_web/live/contribution_type_live/index.ex:113
#, elixir-autogen, elixir-format
msgid "About Contribution Types"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:138
#: lib/mv_web/live/contribution_type_live/index.ex:53
#, elixir-autogen, elixir-format
msgid "Amount"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:48
#, elixir-autogen, elixir-format
msgid "Back to Settings"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:124
#, elixir-autogen, elixir-format
msgid "Can be changed at any time. Amount changes affect future periods only."
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:77
#, elixir-autogen, elixir-format
msgid "Cannot delete - members assigned"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:83
#, elixir-autogen, elixir-format
msgid "Change Contribution Type"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:42
#, elixir-autogen, elixir-format
msgid "Configure global settings for membership contributions."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:34
#: lib/mv_web/live/contribution_settings_live.ex:27
#: lib/mv_web/live/contribution_settings_live.ex:40
#, elixir-autogen, elixir-format
msgid "Contribution Settings"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:62
#, elixir-autogen, elixir-format
msgid "Contribution Start"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:32
#: lib/mv_web/live/contribution_type_live/index.ex:25
#: lib/mv_web/live/contribution_type_live/index.ex:36
#, elixir-autogen, elixir-format
msgid "Contribution Types"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:224
#, elixir-autogen, elixir-format
msgid "Contribution start"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:41
#, elixir-autogen, elixir-format
msgid "Contribution type"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:117
#, elixir-autogen, elixir-format
msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:30
#, elixir-autogen, elixir-format
msgid "Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:39
#, elixir-autogen, elixir-format
msgid "Contributions for %{name}"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:159
#, elixir-autogen, elixir-format
msgid "Current"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:60
#, elixir-autogen, elixir-format
msgid "Default Contribution Type"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:133
#, elixir-autogen, elixir-format
msgid "Deletion"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:173
#, elixir-autogen, elixir-format
msgid "Example: Member Contribution View"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:113
#, elixir-autogen, elixir-format
msgid "Examples"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:262
#: lib/mv_web/live/contribution_type_live/index.ex:172
#, elixir-autogen, elixir-format
msgid "Family"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:128
#, elixir-autogen, elixir-format
msgid "Fixed after creation. Members can only switch between types with the same interval."
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:228
#, elixir-autogen, elixir-format
msgid "Generated periods"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:52
#, elixir-autogen, elixir-format
msgid "Global Settings"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:343
#: lib/mv_web/live/contribution_settings_live.ex:275
#: lib/mv_web/live/contribution_type_live/index.ex:203
#, elixir-autogen, elixir-format
msgid "Half-yearly"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:181
#, elixir-autogen, elixir-format
msgid "Half-yearly contribution for supporting members"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:87
#: lib/mv_web/live/contribution_type_live/index.ex:188
#, elixir-autogen, elixir-format
msgid "Honorary"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:85
#, elixir-autogen, elixir-format
msgid "Include joining period"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:137
#: lib/mv_web/live/contribution_type_live/index.ex:57
#: lib/mv_web/live/contribution_type_live/index.ex:127
#, elixir-autogen, elixir-format
msgid "Interval"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:220
#, elixir-autogen, elixir-format
msgid "Joining date"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:331
#, elixir-autogen, elixir-format
msgid "Joining year - reduced to 0"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:38
#, elixir-autogen, elixir-format
msgid "Manage contribution types for membership fees."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:116
#, elixir-autogen, elixir-format
msgid "Mark as Paid"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:120
#, elixir-autogen, elixir-format
msgid "Mark as Suspended"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:124
#, elixir-autogen, elixir-format
msgid "Mark as Unpaid"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "Member Contributions"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:122
#, elixir-autogen, elixir-format
msgid "Member pays for the year they joined"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:155
#, elixir-autogen, elixir-format
msgid "Member pays from the joining month"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:144
#, elixir-autogen, elixir-format
msgid "Member pays from the next full quarter"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:133
#, elixir-autogen, elixir-format
msgid "Member pays from the next full year"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:43
#, elixir-autogen, elixir-format
msgid "Member since"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:92
#, elixir-autogen, elixir-format
msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:341
#: lib/mv_web/live/contribution_settings_live.ex:273
#: lib/mv_web/live/contribution_type_live/index.ex:201
#, elixir-autogen, elixir-format
msgid "Monthly"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:150
#, elixir-autogen, elixir-format
msgid "Monthly Interval - Joining Period Included"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:165
#, elixir-autogen, elixir-format
msgid "Monthly fee for students and trainees"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:123
#, elixir-autogen, elixir-format
msgid "Name & Amount"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:42
#, elixir-autogen, elixir-format
msgid "New Contribution Type"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:189
#, elixir-autogen, elixir-format
msgid "No fee for honorary members"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:134
#, elixir-autogen, elixir-format
msgid "Only possible if no members are assigned to this type."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:70
#, elixir-autogen, elixir-format
msgid "Open Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:301
#, elixir-autogen, elixir-format
msgid "Paid via bank transfer"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:225
#: lib/mv_web/live/contribution_settings_live.ex:197
#: lib/mv_web/live/contribution_type_live/index.ex:97
#, elixir-autogen, elixir-format
msgid "Preview Mockup"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:342
#: lib/mv_web/live/contribution_settings_live.ex:274
#: lib/mv_web/live/contribution_type_live/index.ex:202
#, elixir-autogen, elixir-format
msgid "Quarterly"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:139
#, elixir-autogen, elixir-format
msgid "Quarterly Interval - Joining Period Excluded"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:173
#, elixir-autogen, elixir-format
msgid "Quarterly fee for family memberships"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:86
#: lib/mv_web/live/contribution_settings_live.ex:250
#: lib/mv_web/live/contribution_type_live/index.ex:156
#, elixir-autogen, elixir-format
msgid "Reduced"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:157
#, elixir-autogen, elixir-format
msgid "Reduced fee for unemployed, pensioners, or low income"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:275
#: lib/mv_web/live/contribution_settings_live.ex:244
#: lib/mv_web/live/contribution_type_live/index.ex:148
#, elixir-autogen, elixir-format
msgid "Regular"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:204
#, elixir-autogen, elixir-format
msgid "Reopen"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:176
#, elixir-autogen, elixir-format
msgid "See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:149
#, elixir-autogen, elixir-format
msgid "Standard membership fee for regular members"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:139
#, elixir-autogen, elixir-format
msgid "Status"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:256
#: lib/mv_web/live/contribution_type_live/index.ex:164
#, elixir-autogen, elixir-format
msgid "Student"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:180
#, elixir-autogen, elixir-format
msgid "Supporting Member"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:195
#, elixir-autogen, elixir-format
msgid "Suspend"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:259
#, elixir-autogen, elixir-format
msgid "Suspended"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:69
#, elixir-autogen, elixir-format
msgid "This contribution type is automatically assigned to all new members. Can be changed individually per member."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:227
#: lib/mv_web/live/contribution_settings_live.ex:199
#: lib/mv_web/live/contribution_type_live/index.ex:99
#, elixir-autogen, elixir-format
msgid "This page is not functional and only displays the planned features."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:136
#, elixir-autogen, elixir-format
msgid "Time Period"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:66
#, elixir-autogen, elixir-format
msgid "Total Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:250
#, elixir-autogen, elixir-format
msgid "Unpaid"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:183
#, elixir-autogen, elixir-format
msgid "View Example Member"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:90
#, elixir-autogen, elixir-format
msgid "When active: Members pay from the period of their joining."
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:93
#, elixir-autogen, elixir-format
msgid "When inactive: Members pay from the next full period after joining."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:98
#, elixir-autogen, elixir-format
msgid "Why are not all contribution types shown?"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:85
#: lib/mv_web/live/contribution_period_live/show.ex:86
#: lib/mv_web/live/contribution_period_live/show.ex:87
#: lib/mv_web/live/contribution_period_live/show.ex:344
#: lib/mv_web/live/contribution_settings_live.ex:276
#: lib/mv_web/live/contribution_type_live/index.ex:204
#, elixir-autogen, elixir-format
msgid "Yearly"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:128
#, elixir-autogen, elixir-format
msgid "Yearly Interval - Joining Period Excluded"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:117
#, elixir-autogen, elixir-format
msgid "Yearly Interval - Joining Period Included"
msgstr ""

View file

@ -11,13 +11,14 @@ msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/mv_web/components/core_components.ex:386
#: lib/mv_web/components/core_components.ex:387
#: lib/mv_web/live/contribution_period_live/show.ex:141
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:248
#: lib/mv_web/live/user_live/index.html.heex:72
#: lib/mv_web/live/user_live/index.html.heex:71
#, elixir-autogen, elixir-format
msgid "Are you sure?"
msgstr ""
@ -34,32 +35,35 @@ msgstr ""
msgid "City"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:78
#: lib/mv_web/live/member_live/index.html.heex:250
#: lib/mv_web/live/user_live/index.html.heex:74
#: lib/mv_web/live/user_live/index.html.heex:73
#, elixir-autogen, elixir-format
msgid "Delete"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:241
#: lib/mv_web/live/contribution_type_live/index.ex:66
#: lib/mv_web/live/member_live/index.html.heex:242
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/index.html.heex:66
#: lib/mv_web/live/user_live/form.ex:267
#: lib/mv_web/live/user_live/index.html.heex:65
#, elixir-autogen, elixir-format
msgid "Edit"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:242
#: lib/mv_web/live/member_live/show.ex:40
#: lib/mv_web/live/member_live/show.ex:197
#, elixir-autogen, elixir-format
msgid "Edit Member"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:58
#: lib/mv_web/live/member_live/form.ex:106
#: lib/mv_web/live/member_live/index.html.heex:112
#: lib/mv_web/live/member_live/show.ex:75
#: lib/mv_web/live/user_live/form.ex:46
#: lib/mv_web/live/user_live/index.html.heex:44
#: lib/mv_web/live/user_live/show.ex:50
#: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "Email"
msgstr ""
@ -90,7 +94,7 @@ msgid "New Member"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:239
#: lib/mv_web/live/user_live/index.html.heex:63
#: lib/mv_web/live/user_live/index.html.heex:62
#, elixir-autogen, elixir-format
msgid "Show"
msgstr ""
@ -121,6 +125,7 @@ msgstr ""
msgid "House Number"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:140
#: lib/mv_web/live/member_live/form.ex:126
#: lib/mv_web/live/member_live/show.ex:124
#, elixir-autogen, elixir-format
@ -129,6 +134,8 @@ msgstr ""
#: lib/mv_web/live/components/payment_filter_component.ex:94
#: lib/mv_web/live/components/payment_filter_component.ex:144
#: lib/mv_web/live/contribution_period_live/show.ex:186
#: lib/mv_web/live/contribution_period_live/show.ex:241
#: lib/mv_web/live/member_live/form.ex:199
#: lib/mv_web/live/member_live/index.html.heex:229
#: lib/mv_web/live/member_live/show.ex:162
@ -158,7 +165,7 @@ msgstr ""
#: lib/mv_web/live/global_settings_live.ex:55
#: lib/mv_web/live/member_live/form.ex:48
#: lib/mv_web/live/member_live/form.ex:210
#: lib/mv_web/live/user_live/form.ex:248
#: lib/mv_web/live/user_live/form.ex:249
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
@ -170,8 +177,8 @@ msgid "Street"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:61
#: lib/mv_web/live/member_live/show.ex:303
#: lib/mv_web/live/member_live/index/formatter.ex:62
#: lib/mv_web/live/member_live/show.ex:290
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
@ -182,8 +189,8 @@ msgid "Show Member"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:234
#: lib/mv_web/live/member_live/index/formatter.ex:60
#: lib/mv_web/live/member_live/show.ex:303
#: lib/mv_web/live/member_live/index/formatter.ex:61
#: lib/mv_web/live/member_live/show.ex:290
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr ""
@ -241,7 +248,7 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/index.ex:120
#: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/member_live/form.ex:208
#: lib/mv_web/live/user_live/form.ex:251
#: lib/mv_web/live/user_live/form.ex:252
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
@ -261,22 +268,17 @@ msgstr ""
msgid "Edit User"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:53
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Enabled"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:49
#, elixir-autogen, elixir-format
msgid "ID"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:62
#, elixir-autogen, elixir-format
msgid "Immutable"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:102
#: lib/mv_web/components/layouts/navbar.ex:113
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr ""
@ -288,18 +290,19 @@ msgid "Listing Users"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:60
#: lib/mv_web/live/member_live/form.ex:242
#, elixir-autogen, elixir-format
msgid "Member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:25
#: lib/mv_web/live/member_live/index.ex:73
#: lib/mv_web/live/contribution_type_live/index.ex:61
#: lib/mv_web/live/member_live/index.ex:74
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
msgid "Members"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:48
#: lib/mv_web/live/custom_field_live/form.ex:51
#, elixir-autogen, elixir-format
msgid "Name"
@ -310,16 +313,12 @@ msgstr ""
msgid "New User"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:53
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "Not enabled"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format, fuzzy
msgid "Not set"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:207
#: lib/mv_web/live/user_live/form.ex:107
#: lib/mv_web/live/user_live/form.ex:115
#: lib/mv_web/live/user_live/form.ex:224
@ -327,18 +326,12 @@ msgstr ""
msgid "Note"
msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "OIDC ID"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:52
#: lib/mv_web/live/user_live/show.ex:50
#, elixir-autogen, elixir-format
msgid "Password Authentication"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:95
#: lib/mv_web/components/layouts/navbar.ex:106
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
@ -358,17 +351,17 @@ msgstr ""
msgid "Select member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:99
#: lib/mv_web/components/layouts/navbar.ex:110
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:249
#: lib/mv_web/live/user_live/form.ex:250
#, elixir-autogen, elixir-format, fuzzy
msgid "Save User"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:79
#: lib/mv_web/live/user_live/show.ex:77
#, elixir-autogen, elixir-format, fuzzy
msgid "Show User"
msgstr ""
@ -388,7 +381,7 @@ msgstr ""
msgid "Use this form to manage user records in your database."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:266
#: lib/mv_web/live/user_live/form.ex:268
#: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "User"
@ -416,8 +409,7 @@ msgstr ""
msgid "descending"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:241
#: lib/mv_web/live/user_live/form.ex:265
#: lib/mv_web/live/user_live/form.ex:267
#, elixir-autogen, elixir-format
msgid "New"
msgstr ""
@ -493,8 +485,8 @@ msgid "User will be created without a password. Check 'Set Password' to add one.
msgstr "User will be created without a password. Check 'Set Password' to add one."
#: lib/mv_web/live/user_live/form.ex:126
#: lib/mv_web/live/user_live/index.html.heex:53
#: lib/mv_web/live/user_live/show.ex:55
#: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:53
#, elixir-autogen, elixir-format, fuzzy
msgid "Linked Member"
msgstr ""
@ -504,8 +496,8 @@ msgstr ""
msgid "Linked User"
msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:57
#: lib/mv_web/live/user_live/show.ex:65
#: lib/mv_web/live/user_live/index.html.heex:56
#: lib/mv_web/live/user_live/show.ex:63
#, elixir-autogen, elixir-format
msgid "No member linked"
msgstr ""
@ -526,14 +518,14 @@ msgstr ""
msgid "Back to users list"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:33
#: lib/mv_web/components/layouts/navbar.ex:39
#: lib/mv_web/components/layouts/navbar.ex:44
#: lib/mv_web/components/layouts/navbar.ex:50
#, elixir-autogen, elixir-format, fuzzy
msgid "Select language"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:46
#: lib/mv_web/components/layouts/navbar.ex:66
#: lib/mv_web/components/layouts/navbar.ex:57
#: lib/mv_web/components/layouts/navbar.ex:77
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr ""
@ -701,6 +693,7 @@ msgstr ""
msgid "Manage global settings for the association."
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:102
#: lib/mv_web/live/global_settings_live.ex:56
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Settings"
@ -721,7 +714,7 @@ msgstr ""
msgid "Available members"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:357
#: lib/mv_web/live/user_live/form.ex:359
#, elixir-autogen, elixir-format
msgid "Failed to link member: %{error}"
msgstr ""
@ -761,7 +754,7 @@ msgstr ""
msgid "Unlinking scheduled"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:159
#: lib/mv_web/live/member_live/index.ex:160
#, elixir-autogen, elixir-format
msgid "Copied %{count} email address to clipboard"
msgid_plural "Copied %{count} email addresses to clipboard"
@ -778,12 +771,12 @@ msgstr ""
msgid "Copy emails"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:148
#: lib/mv_web/live/member_live/index.ex:149
#, elixir-autogen, elixir-format
msgid "No email addresses found"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:145
#: lib/mv_web/live/member_live/index.ex:146
#, elixir-autogen, elixir-format, fuzzy
msgid "No members selected"
msgstr ""
@ -798,7 +791,7 @@ msgstr ""
msgid "Open in email program"
msgstr ""
#: lib/mv_web/live/member_live/index.ex:168
#: lib/mv_web/live/member_live/index.ex:169
#, elixir-autogen, elixir-format
msgid "Tip: Paste email addresses into the BCC field for privacy compliance"
msgstr ""
@ -924,11 +917,458 @@ msgstr ""
msgid "yearly"
msgstr ""
#: lib/mv_web/live/member_live/form.ex:242
#, elixir-autogen, elixir-format
msgid "Create Member"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:107
#, elixir-autogen, elixir-format
msgid "%{count} period selected"
msgid_plural "%{count} periods selected"
msgstr[0] ""
msgstr[1] ""
#: lib/mv_web/live/contribution_type_live/index.ex:113
#, elixir-autogen, elixir-format
msgid "About Contribution Types"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:138
#: lib/mv_web/live/contribution_type_live/index.ex:53
#, elixir-autogen, elixir-format
msgid "Amount"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:48
#, elixir-autogen, elixir-format
msgid "Back to Settings"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:124
#, elixir-autogen, elixir-format
msgid "Can be changed at any time. Amount changes affect future periods only."
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:77
#, elixir-autogen, elixir-format
msgid "Cannot delete - members assigned"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:83
#, elixir-autogen, elixir-format
msgid "Change Contribution Type"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:42
#, elixir-autogen, elixir-format
msgid "Configure global settings for membership contributions."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:34
#: lib/mv_web/live/contribution_settings_live.ex:27
#: lib/mv_web/live/contribution_settings_live.ex:40
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Settings"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:62
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Start"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:32
#: lib/mv_web/live/contribution_type_live/index.ex:25
#: lib/mv_web/live/contribution_type_live/index.ex:36
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Types"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:224
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution start"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:41
#, elixir-autogen, elixir-format, fuzzy
msgid "Contribution type"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:117
#, elixir-autogen, elixir-format
msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:30
#, elixir-autogen, elixir-format, fuzzy
msgid "Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:39
#, elixir-autogen, elixir-format, fuzzy
msgid "Contributions for %{name}"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:159
#, elixir-autogen, elixir-format
msgid "Current"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:60
#, elixir-autogen, elixir-format
msgid "Default Contribution Type"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:133
#, elixir-autogen, elixir-format, fuzzy
msgid "Deletion"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:173
#, elixir-autogen, elixir-format
msgid "Example: Member Contribution View"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:113
#, elixir-autogen, elixir-format
msgid "Examples"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:262
#: lib/mv_web/live/contribution_type_live/index.ex:172
#, elixir-autogen, elixir-format
msgid "Family"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:128
#, elixir-autogen, elixir-format
msgid "Fixed after creation. Members can only switch between types with the same interval."
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:228
#, elixir-autogen, elixir-format
msgid "Generated periods"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:52
#, elixir-autogen, elixir-format, fuzzy
msgid "Global Settings"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:343
#: lib/mv_web/live/contribution_settings_live.ex:275
#: lib/mv_web/live/contribution_type_live/index.ex:203
#, elixir-autogen, elixir-format
msgid "Half-yearly"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:181
#, elixir-autogen, elixir-format
msgid "Half-yearly contribution for supporting members"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:87
#: lib/mv_web/live/contribution_type_live/index.ex:188
#, elixir-autogen, elixir-format
msgid "Honorary"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:85
#, elixir-autogen, elixir-format
msgid "Include joining period"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:137
#: lib/mv_web/live/contribution_type_live/index.ex:57
#: lib/mv_web/live/contribution_type_live/index.ex:127
#, elixir-autogen, elixir-format
msgid "Interval"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:220
#, elixir-autogen, elixir-format, fuzzy
msgid "Joining date"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:331
#, elixir-autogen, elixir-format
msgid "Joining year - reduced to 0"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:38
#, elixir-autogen, elixir-format
msgid "Manage contribution types for membership fees."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:116
#, elixir-autogen, elixir-format
msgid "Mark as Paid"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:120
#, elixir-autogen, elixir-format
msgid "Mark as Suspended"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:124
#, elixir-autogen, elixir-format
msgid "Mark as Unpaid"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:26
#, elixir-autogen, elixir-format
msgid "Member Contributions"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:122
#, elixir-autogen, elixir-format
msgid "Member pays for the year they joined"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:155
#, elixir-autogen, elixir-format
msgid "Member pays from the joining month"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:144
#, elixir-autogen, elixir-format
msgid "Member pays from the next full quarter"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:133
#, elixir-autogen, elixir-format
msgid "Member pays from the next full year"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:43
#, elixir-autogen, elixir-format, fuzzy
msgid "Member since"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:92
#, elixir-autogen, elixir-format
msgid "Members can only switch between contribution types with the same payment interval (e.g., yearly to yearly). This prevents complex period overlaps."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:341
#: lib/mv_web/live/contribution_settings_live.ex:273
#: lib/mv_web/live/contribution_type_live/index.ex:201
#, elixir-autogen, elixir-format, fuzzy
msgid "Monthly"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:150
#, elixir-autogen, elixir-format
msgid "Monthly Interval - Joining Period Included"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:165
#, elixir-autogen, elixir-format
msgid "Monthly fee for students and trainees"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:123
#, elixir-autogen, elixir-format
msgid "Name & Amount"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:42
#, elixir-autogen, elixir-format, fuzzy
msgid "New Contribution Type"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:189
#, elixir-autogen, elixir-format
msgid "No fee for honorary members"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:134
#, elixir-autogen, elixir-format
msgid "Only possible if no members are assigned to this type."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:70
#, elixir-autogen, elixir-format
msgid "Open Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:301
#, elixir-autogen, elixir-format
msgid "Paid via bank transfer"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:225
#: lib/mv_web/live/contribution_settings_live.ex:197
#: lib/mv_web/live/contribution_type_live/index.ex:97
#, elixir-autogen, elixir-format
msgid "Preview Mockup"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:342
#: lib/mv_web/live/contribution_settings_live.ex:274
#: lib/mv_web/live/contribution_type_live/index.ex:202
#, elixir-autogen, elixir-format
msgid "Quarterly"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:139
#, elixir-autogen, elixir-format
msgid "Quarterly Interval - Joining Period Excluded"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:173
#, elixir-autogen, elixir-format
msgid "Quarterly fee for family memberships"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:86
#: lib/mv_web/live/contribution_settings_live.ex:250
#: lib/mv_web/live/contribution_type_live/index.ex:156
#, elixir-autogen, elixir-format
msgid "Reduced"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:157
#, elixir-autogen, elixir-format
msgid "Reduced fee for unemployed, pensioners, or low income"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:275
#: lib/mv_web/live/contribution_settings_live.ex:244
#: lib/mv_web/live/contribution_type_live/index.ex:148
#, elixir-autogen, elixir-format
msgid "Regular"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:204
#, elixir-autogen, elixir-format
msgid "Reopen"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:176
#, elixir-autogen, elixir-format
msgid "See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods."
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:149
#, elixir-autogen, elixir-format
msgid "Standard membership fee for regular members"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:139
#, elixir-autogen, elixir-format
msgid "Status"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:256
#: lib/mv_web/live/contribution_type_live/index.ex:164
#, elixir-autogen, elixir-format
msgid "Student"
msgstr ""
#: lib/mv_web/live/contribution_type_live/index.ex:180
#, elixir-autogen, elixir-format
msgid "Supporting Member"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:195
#, elixir-autogen, elixir-format
msgid "Suspend"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:259
#, elixir-autogen, elixir-format
msgid "Suspended"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:69
#, elixir-autogen, elixir-format
msgid "This contribution type is automatically assigned to all new members. Can be changed individually per member."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:227
#: lib/mv_web/live/contribution_settings_live.ex:199
#: lib/mv_web/live/contribution_type_live/index.ex:99
#, elixir-autogen, elixir-format
msgid "This page is not functional and only displays the planned features."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:136
#, elixir-autogen, elixir-format
msgid "Time Period"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:66
#, elixir-autogen, elixir-format
msgid "Total Contributions"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:250
#, elixir-autogen, elixir-format
msgid "Unpaid"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:183
#, elixir-autogen, elixir-format
msgid "View Example Member"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:90
#, elixir-autogen, elixir-format
msgid "When active: Members pay from the period of their joining."
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:93
#, elixir-autogen, elixir-format
msgid "When inactive: Members pay from the next full period after joining."
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:98
#, elixir-autogen, elixir-format
msgid "Why are not all contribution types shown?"
msgstr ""
#: lib/mv_web/live/contribution_period_live/show.ex:85
#: lib/mv_web/live/contribution_period_live/show.ex:86
#: lib/mv_web/live/contribution_period_live/show.ex:87
#: lib/mv_web/live/contribution_period_live/show.ex:344
#: lib/mv_web/live/contribution_settings_live.ex:276
#: lib/mv_web/live/contribution_type_live/index.ex:204
#, elixir-autogen, elixir-format, fuzzy
msgid "Yearly"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:128
#, elixir-autogen, elixir-format
msgid "Yearly Interval - Joining Period Excluded"
msgstr ""
#: lib/mv_web/live/contribution_settings_live.ex:117
#, elixir-autogen, elixir-format
msgid "Yearly Interval - Joining Period Included"
msgstr ""
#~ #: lib/mv_web/live/user_live/show.ex:49
#~ #, elixir-autogen, elixir-format
#~ msgid "ID"
#~ msgstr ""
#~ #: lib/mv_web/live/member_live/show.ex:47
#~ #, elixir-autogen, elixir-format
#~ msgid "Id"
#~ msgstr ""
#~ #: lib/mv_web/live/user_live/show.ex:51
#~ #, elixir-autogen, elixir-format, fuzzy
#~ msgid "Not set"
#~ msgstr ""
#~ #: lib/mv_web/live/user_live/index.html.heex:52
#~ #: lib/mv_web/live/user_live/show.ex:51
#~ #, elixir-autogen, elixir-format
#~ msgid "OIDC ID"
#~ msgstr ""
#~ #: lib/mv_web/live/member_live/show.ex:33
#~ #, elixir-autogen, elixir-format
#~ msgid "This is a member record from your database."

View file

@ -90,8 +90,6 @@ defmodule MvWeb.ProfileNavigationTest do
# Verify we're on the correct profile page with OIDC specific information
{:ok, _profile_view, html} = live(conn, "/users/#{user.id}")
assert html =~ to_string(user.email)
# OIDC ID should be visible
assert html =~ "oidc_123"
# Password auth should be disabled for OIDC users
assert html =~ "Not enabled"
end

View file

@ -231,8 +231,8 @@ defmodule MvWeb.MemberLive.IndexCustomFieldsDisplayTest do
conn = conn_with_oidc_user(conn)
{:ok, _view, html} = live(conn, "/members")
# Date should be displayed in readable format
assert html =~ "1990" or html =~ "1990-05-15" or html =~ "15.05.1990"
# Date should be displayed in European format (dd.mm.yyyy)
assert html =~ "15.05.1990"
end
test "formats email custom field values correctly", %{conn: conn, member1: _member1} do

View file

@ -33,8 +33,6 @@ defmodule MvWeb.UserLive.IndexTest do
assert html =~ "alice@example.com"
assert html =~ "bob@example.com"
assert html =~ "alice123"
assert html =~ "bob456"
end
test "shows correct action links", %{conn: conn} do
@ -386,10 +384,6 @@ defmodule MvWeb.UserLive.IndexTest do
# Should still show the table structure
assert html =~ "Email"
assert html =~ "OIDC ID"
# Should show the authenticated user at minimum
# Matches the generated email pattern oidc.user{unique_id}@example.com
assert html =~ "oidc.user"
end
test "handles users with missing OIDC ID", %{conn: conn} do

View file

@ -123,7 +123,13 @@ defmodule MvWeb.ConnCase do
end
setup tags do
Mv.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
pid = Mv.DataCase.setup_sandbox(tags)
conn = Phoenix.ConnTest.build_conn()
# Set metadata for Phoenix.Ecto.SQL.Sandbox plug to allow LiveView processes
# to share the test's database connection in async tests
conn = Plug.Conn.put_private(conn, :ecto_sandbox, pid)
{:ok, conn: conn}
end
end

View file

@ -34,10 +34,12 @@ defmodule Mv.DataCase do
@doc """
Sets up the sandbox based on the test tags.
Returns the owner pid for use with Phoenix.Ecto.SQL.Sandbox.
"""
def setup_sandbox(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Mv.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
pid
end
@doc """