diff --git a/.gitignore b/.gitignore index 9517a21..63ff39e 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,3 @@ npm-debug.log .env .elixir_ls/ - -# Docker secrets directory (generated by `just init-secrets`) -/secrets/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 28b4a37..71d9147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,6 @@ 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) diff --git a/Justfile b/Justfile index 25fb35c..907283f 100644 --- a/Justfile +++ b/Justfile @@ -84,33 +84,4 @@ regen-migrations migration_name commit_hash='': 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; 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 \ No newline at end of file + rm -rf _build \ No newline at end of file diff --git a/README.md b/README.md index 14435db..6db7980 100644 --- a/README.md +++ b/README.md @@ -217,13 +217,6 @@ 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= - - # 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): @@ -257,7 +250,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** — 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. +4. **Use secure secrets management** (environment variables, Docker secrets, vault) 5. **Configure database backups** diff --git a/config/runtime.exs b/config/runtime.exs index 71138ef..c50356c 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -7,75 +7,6 @@ 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 @@ -90,7 +21,12 @@ if System.get_env("PHX_SERVER") do end if config_env() == :prod do - database_url = build_database_url.() + database_url = + System.get_env("DATABASE_URL") || + raise """ + environment variable DATABASE_URL is missing. + For example: ecto://USER:PASS@HOST/DATABASE + """ maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] @@ -105,12 +41,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 = - 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 - """) + System.get_env("SECRET_KEY_BASE") || + raise """ + environment variable SECRET_KEY_BASE 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") @@ -118,47 +54,32 @@ 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: oidc_client_id || "mv", - base_url: oidc_base_url || "http://localhost:8080/auth/v1", - client_secret: client_secret, + 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"), 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 = - 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 - """) + System.get_env("TOKEN_SIGNING_SECRET") || + raise """ + environment variable TOKEN_SIGNING_SECRET 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: [ - # Bind on all IPv4 interfaces. - # Use {0, 0, 0, 0, 0, 0, 0, 0} for IPv6, or {127, 0, 0, 1} for localhost only. + # Enable IPv6 and bind on all interfaces. + # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 - ip: {0, 0, 0, 0}, + # for details about using IPv6 vs IPv4 and loopback vs public addresses. + ip: {0, 0, 0, 0, 0, 0, 0, 0}, port: port ], secret_key_base: secret_key_base, diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b4b7a1f..0bb2840 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -2,32 +2,21 @@ services: app: image: git.local-it.org/local-it/mitgliederverwaltung:latest container_name: mv-prod-app - ports: - - "4001:4001" + # Use host network for local testing to access localhost:8080 (Rauthy) + # In real production, remove this and use external OIDC provider + network_mode: host environment: - # 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}" + 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}" PORT: "4001" PHX_SERVER: "true" - # Rauthy OIDC config - use host.docker.internal to reach host services + # Rauthy OIDC config - uses localhost because of host network mode OIDC_CLIENT_ID: "mv" - OIDC_BASE_URL: "http://host.docker.internal:8080/auth/v1" - OIDC_CLIENT_SECRET_FILE: "/run/secrets/oidc_client_secret" + OIDC_BASE_URL: "http://localhost:8080/auth/v1" + OIDC_CLIENT_SECRET: "${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 @@ -37,25 +26,13 @@ services: container_name: mv-prod-db environment: POSTGRES_USER: postgres - POSTGRES_PASSWORD_FILE: /run/secrets/db_password + POSTGRES_PASSWORD: postgres 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: diff --git a/docs/contributions-architecture.md b/docs/contributions-architecture.md deleted file mode 100644 index 3718a3b..0000000 --- a/docs/contributions-architecture.md +++ /dev/null @@ -1,653 +0,0 @@ -# Membership Contributions - Technical Architecture - -**Project:** Mila - Membership Management System -**Feature:** Membership Contribution Management -**Version:** 1.0 -**Last Updated:** 2025-11-27 -**Status:** Architecture Design - Ready for Implementation - ---- - -## Purpose - -This document defines the technical architecture for the Membership Contributions system. It focuses on architectural decisions, patterns, module structure, and integration points **without** concrete implementation details. - -**Related Documents:** -- [contributions-overview.md](./contributions-overview.md) - Business logic and requirements -- [database-schema-readme.md](./database-schema-readme.md) - Database documentation -- [database_schema.dbml](./database_schema.dbml) - Database schema definition - ---- - -## Table of Contents - -1. [Architecture Principles](#architecture-principles) -2. [Domain Structure](#domain-structure) -3. [Data Architecture](#data-architecture) -4. [Business Logic Architecture](#business-logic-architecture) -5. [Integration Points](#integration-points) -6. [Acceptance Criteria](#acceptance-criteria) -7. [Testing Strategy](#testing-strategy) -8. [Security Considerations](#security-considerations) -9. [Performance Considerations](#performance-considerations) - ---- - -## Architecture Principles - -### Core Design Decisions - -1. **Single Responsibility:** - - Each module has one clear responsibility - - Period generation separated from status management - - Calendar logic isolated in dedicated module - -2. **No Redundancy:** - - No `period_end` field (calculated from `period_start` + `interval`) - - No `interval_type` field (read from `contribution_type.interval`) - - Eliminates data inconsistencies - -3. **Immutability Where Important:** - - `contribution_type.interval` cannot be changed after creation - - Prevents complex migration scenarios - - Enforced via Ash change validation - -4. **Historical Accuracy:** - - `amount` stored per period for audit trail - - Enables tracking of contribution changes over time - - Old periods retain original amounts - -5. **Calendar-Based Periods:** - - All periods aligned to calendar boundaries - - Simplifies date calculations - - Predictable period generation - ---- - -## Domain Structure - -### Ash Domain: `Mv.Contributions` - -**Purpose:** Encapsulates all contribution-related resources and logic - -**Resources:** -- `ContributionType` - Contribution type definitions (admin-managed) -- `ContributionPeriod` - Individual contribution periods per member - -**Extensions:** -- Member resource extended with contribution fields - -### Module Organization - -``` -lib/ -├── contributions/ -│ ├── contributions.ex # Ash domain definition -│ ├── contribution_type.ex # ContributionType resource -│ ├── contribution_period.ex # ContributionPeriod resource -│ └── changes/ -│ ├── prevent_interval_change.ex # Validates interval immutability -│ ├── set_contribution_start_date.ex # Auto-sets start date -│ └── validate_same_interval.ex # Validates interval match on type change -├── mv/ -│ └── contributions/ -│ ├── period_generator.ex # Period generation algorithm -│ └── calendar_periods.ex # Calendar period calculations -└── membership/ - └── member.ex # Extended with contribution relationships -``` - -### Separation of Concerns - -**Domain Layer (Ash Resources):** -- Data validation -- Relationship management -- Policy enforcement -- Action definitions - -**Business Logic Layer (`Mv.Contributions`):** -- Period generation algorithm -- Calendar calculations -- Date boundary handling -- Status transitions - -**UI Layer (LiveView):** -- User interaction -- Display logic -- Authorization checks -- Form handling - ---- - -## Data Architecture - -### Database Schema Extensions - -**See:** [database-schema-readme.md](./database-schema-readme.md) and [database_schema.dbml](./database_schema.dbml) for complete schema documentation. - -### New Tables - -1. **`contribution_types`** - - Purpose: Define contribution types with fixed intervals - - Key Constraint: `interval` field immutable after creation - - Relationships: has_many members, has_many contribution_periods - -2. **`contribution_periods`** - - Purpose: Individual contribution periods for members - - Key Design: NO `period_end` or `interval_type` fields (calculated) - - Relationships: belongs_to member, belongs_to contribution_type - - Composite uniqueness: One period per member per period_start - -### Member Table Extensions - -**Fields Added:** -- `contribution_type_id` (FK, NOT NULL with default from settings) -- `contribution_start_date` (Date, nullable) - -**Existing Fields Used:** -- `joined_at` - For calculating contribution start -- `left_at` - For limiting period generation -- These fields must remain member fields and should not be replaced by custom fields in the future - -### Settings Integration - -**Global Settings:** -- `contributions.include_joining_period` (Boolean) -- `contributions.default_contribution_type_id` (UUID) - -**Storage:** Existing settings mechanism (TBD: dedicated table or configuration resource) - -### Foreign Key Behaviors - -| Relationship | On Delete | Rationale | -|--------------|-----------|-----------| -| `contribution_periods.member_id → members.id` | CASCADE | Remove periods when member deleted | -| `contribution_periods.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if periods exist | -| `members.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if assigned to members | - ---- - -## Business Logic Architecture - -### Period Generation System - -**Component:** `Mv.Contributions.PeriodGenerator` - -**Responsibilities:** -- Calculate which periods should exist for a member -- Generate missing periods -- Respect contribution_start_date and left_at boundaries -- Skip existing periods (idempotent) - -**Triggers:** -1. Member contribution type assigned (via Ash change) -2. Member created with contribution type (via Ash change) -3. Scheduled job runs (daily/weekly cron) -4. Admin manual regeneration (UI action) - -**Algorithm Steps:** -1. Retrieve member with contribution_type and dates -2. Determine first period start (based on contribution_start_date) -3. Calculate all period starts from first to today (or left_at) -4. Query existing periods for member -5. Generate missing periods with current contribution_type.amount -6. Insert new periods (batch operation) - -**Edge Case Handling:** -- If contribution_start_date is NULL: Calculate from joined_at + global setting -- If left_at is set: Stop generation at left_at -- If contribution_type changes: Handled separately by regeneration logic - -### Calendar Period Calculations - -**Component:** `Mv.Contributions.CalendarPeriods` - -**Responsibilities:** -- Calculate period boundaries based on interval type -- Determine current period -- Determine last completed period -- Calculate period_end from period_start + interval - -**Functions (high-level):** -- `calculate_period_start/3` - Given date and interval, find period start -- `calculate_period_end/2` - Given period_start and interval, calculate end -- `next_period_start/2` - Given period_start and interval, find next -- `is_current_period?/2` - Check if period contains today -- `is_last_completed_period?/2` - Check if period just ended - -**Interval Logic:** -- **Monthly:** Start = 1st of month, End = last day of month -- **Quarterly:** Start = 1st of quarter (Jan/Apr/Jul/Oct), End = last day of quarter -- **Half-yearly:** Start = 1st of half (Jan/Jul), End = last day of half -- **Yearly:** Start = Jan 1st, End = Dec 31st - -### Status Management - -**Component:** Ash actions on `ContributionPeriod` - -**Status Transitions:** -- Simple state machine: unpaid ↔ paid ↔ suspended -- No complex validation (all transitions allowed) -- Permissions checked via Ash policies - -**Actions Required:** -- `mark_as_paid` - Set status to :paid -- `mark_as_suspended` - Set status to :suspended -- `mark_as_unpaid` - Set status to :unpaid (error correction) - -**Bulk Operations:** -- `bulk_mark_as_paid` - Mark multiple periods as paid (efficiency) - - low priority, can be a future issue - -### Contribution Type Change Handling - -**Component:** Ash change on `Member.contribution_type_id` - -**Validation:** -- Check if new type has same interval as old type -- If different: Reject change (MVP constraint) -- If same: Allow change - -**Side Effects on Allowed Change:** -1. Keep all existing periods unchanged -2. Find future unpaid periods -3. Delete future unpaid periods -4. Regenerate periods with new contribution_type_id and amount - -**Implementation Pattern:** -- Use Ash change module to validate -- Use after_action hook to trigger regeneration -- Use transaction to ensure atomicity - ---- - -## Integration Points - -### Member Resource Integration - -**Extension Points:** -1. Add fields via migration -2. Add relationships (belongs_to, has_many) -3. Add calculations (current_period_status, overdue_count) -4. Add changes (auto-set contribution_start_date, validate interval) - -**Backward Compatibility:** -- New fields nullable or with defaults -- Existing members get default contribution type from settings -- No breaking changes to existing member functionality - -### Settings System Integration - -**Requirements:** -- Store two global settings -- Provide UI for admin to modify -- Default values if not set -- Validation (e.g., default_contribution_type_id must exist) - -**Access Pattern:** -- Read settings during period generation -- Read settings during member creation -- Write settings only via admin UI - -### Permission System Integration - -**See:** [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md) - -**Required Permissions:** -- `ContributionType.create/update/destroy` - Admin only -- `ContributionType.read` - Admin, Treasurer, Board -- `ContributionPeriod.update` (status changes) - Admin, Treasurer -- `ContributionPeriod.read` - Admin, Treasurer, Board, Own member - -**Policy Patterns:** -- Use existing HasPermission check -- Leverage existing roles (Admin, Kassenwart) -- Member can read own periods (linked via member_id) - -### LiveView Integration - -**New LiveViews Required:** -1. ContributionType index/form (admin) -2. ContributionPeriod table component (member detail view) -3. Settings form section (admin) -4. Member list column (contribution status) - -**Existing LiveViews to Extend:** -- Member detail view: Add contributions section -- Member list view: Add status column -- Settings page: Add contributions section - -**Authorization Helpers:** -- Use existing `can?/3` helper for UI conditionals -- Check permissions before showing actions - ---- - -## Acceptance Criteria - -### ContributionType Resource - -**AC-CT-1:** Admin can create contribution type with name, amount, interval, description -**AC-CT-2:** Interval field is immutable after creation (validation error on change attempt) -**AC-CT-3:** Admin can update name, amount, description (but not interval) -**AC-CT-4:** Cannot delete contribution type if assigned to members -**AC-CT-5:** Cannot delete contribution type if periods exist referencing it -**AC-CT-6:** Interval must be one of: monthly, quarterly, half_yearly, yearly - -### ContributionPeriod Resource - -**AC-CP-1:** Period has period_start, status, amount, notes, member_id, contribution_type_id -**AC-CP-2:** Period_end is calculated, not stored -**AC-CP-3:** Status defaults to :unpaid -**AC-CP-4:** One period per member per period_start (uniqueness constraint) -**AC-CP-5:** Amount is set at generation time from contribution_type.amount -**AC-CP-6:** Periods cascade delete when member deleted -**AC-CP-7:** Admin/Treasurer can change status -**AC-CP-8:** Member can read own periods - -### Member Extensions - -**AC-M-1:** Member has contribution_type_id field (NOT NULL with default) -**AC-M-2:** Member has contribution_start_date field (nullable) -**AC-M-3:** New members get default contribution type from global setting -**AC-M-4:** contribution_start_date auto-set based on joined_at and global setting -**AC-M-5:** Admin can manually override contribution_start_date -**AC-M-6:** Cannot change to contribution type with different interval (MVP) - -### Period Generation - -**AC-PG-1:** Periods generated when member gets contribution type -**AC-PG-2:** Periods generated when member created (via change hook) -**AC-PG-3:** Scheduled job generates missing periods daily -**AC-PG-4:** Generation respects contribution_start_date -**AC-PG-5:** Generation stops at left_at if member exited -**AC-PG-6:** Generation is idempotent (skips existing periods) -**AC-PG-7:** Periods align to calendar boundaries (1st of month/quarter/half/year) -**AC-PG-8:** Amount comes from contribution_type at generation time - -### Calendar Logic - -**AC-CL-1:** Monthly periods: 1st to last day of month -**AC-CL-2:** Quarterly periods: 1st of Jan/Apr/Jul/Oct to last day of quarter -**AC-CL-3:** Half-yearly periods: 1st of Jan/Jul to last day of half -**AC-CL-4:** Yearly periods: Jan 1 to Dec 31 -**AC-CL-5:** Period_end calculated correctly for all interval types -**AC-CL-6:** Current period determined correctly based on today's date -**AC-CL-7:** Last completed period determined correctly - -### Contribution Type Change - -**AC-TC-1:** Can change to type with same interval -**AC-TC-2:** Cannot change to type with different interval (error message) -**AC-TC-3:** On allowed change: future unpaid periods regenerated -**AC-TC-4:** On allowed change: paid/suspended periods unchanged -**AC-TC-5:** On allowed change: amount updated to new type's amount -**AC-TC-6:** Change is atomic (transaction) - -### Settings - -**AC-S-1:** Global setting: include_joining_period (boolean, default true) -**AC-S-2:** Global setting: default_contribution_type_id (UUID, required) -**AC-S-3:** Admin can modify settings via UI -**AC-S-4:** Settings validated (e.g., default type must exist) -**AC-S-5:** Settings applied to new members immediately - -### UI - Member List - -**AC-UI-ML-1:** New column shows contribution status -**AC-UI-ML-2:** Default: Shows last completed period status -**AC-UI-ML-3:** Optional: Toggle to show current period status -**AC-UI-ML-4:** Color coding: green (paid), red (unpaid), gray (suspended) -**AC-UI-ML-5:** Filter: Unpaid in last period -**AC-UI-ML-6:** Filter: Unpaid in current period - -### UI - Member Detail - -**AC-UI-MD-1:** Contributions section shows all periods -**AC-UI-MD-2:** Table columns: Period, Interval, Amount, Status, Actions -**AC-UI-MD-3:** Checkbox per period for bulk marking (low prio) -**AC-UI-MD-4:** "Mark selected as paid" button -**AC-UI-MD-5:** Dropdown to change contribution type (same interval only) -**AC-UI-MD-6:** Warning if different interval selected -**AC-UI-MD-7:** Only show actions if user has permission - -### UI - Contribution Types Admin - -**AC-UI-CTA-1:** List all contribution types -**AC-UI-CTA-2:** Show: Name, Amount, Interval, Member count -**AC-UI-CTA-3:** Create new contribution type form -**AC-UI-CTA-4:** Edit form: Name, Amount, Description editable -**AC-UI-CTA-5:** Edit form: Interval grayed out (not editable) -**AC-UI-CTA-6:** Warning on amount change (explain impact) -**AC-UI-CTA-7:** Cannot delete if members assigned -**AC-UI-CTA-8:** Only admin can access - -### UI - Settings Admin - -**AC-UI-SA-1:** Contributions section in settings -**AC-UI-SA-2:** Dropdown to select default contribution type -**AC-UI-SA-3:** Checkbox: Include joining period -**AC-UI-SA-4:** Explanatory text with examples -**AC-UI-SA-5:** Save button with validation - ---- - -## Testing Strategy - -### Unit Testing - -**Period Generator Tests:** -- Correct period_start calculation for all interval types -- Correct period count from start to end date -- Respects contribution_start_date boundary -- Respects left_at boundary -- Skips existing periods (idempotent) -- Handles edge dates (year boundaries, leap years) - -**Calendar Periods Tests:** -- Period boundaries correct for all intervals -- Period_end calculation correct -- Current period detection -- Last completed period detection -- Next period calculation - -**Validation Tests:** -- Interval immutability enforced -- Same interval validation on type change -- Status transitions allowed -- Uniqueness constraints enforced - -### Integration Testing - -**Period Generation Flow:** -- Member creation triggers generation -- Type assignment triggers generation -- Type change regenerates future periods -- Scheduled job generates missing periods -- Left member stops generation - -**Status Management Flow:** -- Mark single period as paid -- Bulk mark multiple periods (low prio) -- Status transitions work -- Permissions enforced - -**Contribution Type Management:** -- Create type -- Update amount (regeneration triggered) -- Cannot update interval -- Cannot delete if in use - -### LiveView Testing - -**Member List:** -- Status column displays correctly -- Toggle between last/current works -- Filters work correctly -- Color coding applied - -**Member Detail:** -- Periods table displays all periods -- Checkboxes work -- Bulk marking works (low prio) -- Type change validation works -- Actions only shown with permission - -**Admin UI:** -- Type CRUD works -- Settings save correctly -- Validations display errors -- Only authorized users can access - -### Edge Case Testing - -**Interval Change Attempt:** -- Error message displayed -- No data modified -- User can cancel/choose different type - -**Exit with Unpaid:** -- Warning shown -- Option to suspend offered -- Exit completes correctly - -**Amount Change:** -- Warning displayed -- Only future unpaid regenerated -- Historical periods unchanged - -**Date Boundaries:** -- Today = period start handled -- Today = period end handled -- Leap year handled - -### Performance Testing - -**Period Generation:** -- Generate 10 years of monthly periods: < 100ms -- Generate for 1000 members: < 5 seconds -- Idempotent check efficient (no full scan) - -**Member List Query:** -- With status column: < 200ms for 1000 members -- Filters applied efficiently -- No N+1 queries - ---- - -## Security Considerations - -### Authorization - -**Permissions Required:** -- ContributionType management: Admin only -- ContributionPeriod status changes: Admin + Treasurer -- View all periods: Admin + Treasurer + Board -- View own periods: All authenticated users - -**Policy Enforcement:** -- All actions protected by Ash policies -- UI shows/hides based on permissions -- Backend validates permissions (never trust UI alone) - -### Data Integrity - -**Validation Layers:** -1. Database constraints (NOT NULL, UNIQUE, CHECK) -2. Ash validations (business rules) -3. UI validations (user experience) - -**Immutability Protection:** -- Interval change prevented at multiple layers -- Period amounts immutable (audit trail) -- Settings changes logged (future) - -### Audit Trail - -**Tracked Information:** -- Period status changes (who, when) - future enhancement -- Type amount changes (implicit via period amounts) -- Member type assignments (via timestamps) - ---- - -## Performance Considerations - -### Database Indexes - -**Required Indexes:** -- `contribution_periods(member_id)` - For member period lookups -- `contribution_periods(contribution_type_id)` - For type queries -- `contribution_periods(status)` - For unpaid filters -- `contribution_periods(period_start)` - For date range queries -- `contribution_periods(member_id, period_start)` - Composite unique index -- `members(contribution_type_id)` - For type membership count - -### Query Optimization - -**Preloading:** -- Load contribution_type with periods (avoid N+1) -- Load periods when displaying member detail -- Use Ash's load for efficient preloading - -**Calculated Fields:** -- period_end calculated on-demand (not stored) -- current_period_status calculated when needed -- Use Ash calculations for lazy evaluation - -**Pagination:** -- Period list paginated if > 50 periods -- Member list already paginated - -### Caching Strategy - -**No caching needed in MVP:** -- Contribution types rarely change -- Period queries are fast -- Settings read infrequently - -**Future caching if needed:** -- Cache settings in application memory -- Cache contribution types list -- Invalidate on change - -### Scheduled Job Performance - -**Period Generation Job:** -- Run daily or weekly (not hourly) -- Batch members (process 100 at a time) -- Skip members with no changes -- Log failures for retry - ---- - -## Future Enhancements - -### Phase 2: Interval Change Support - -**Architecture Changes:** -- Add logic to handle period overlaps -- Calculate prorata amounts if needed -- More complex validation -- Migration path for existing periods - -### Phase 3: Payment Details - -**Architecture Changes:** -- Add PaymentTransaction resource -- Link transactions to periods -- Support multiple payments per period -- Reconciliation logic - -### Phase 4: vereinfacht.digital Integration - -**Architecture Changes:** -- External API client module -- Webhook handling for transactions -- Automatic matching logic -- Manual review interface - ---- - -**End of Architecture Document** - diff --git a/docs/contributions-overview.md b/docs/contributions-overview.md deleted file mode 100644 index e0c4bc8..0000000 --- a/docs/contributions-overview.md +++ /dev/null @@ -1,527 +0,0 @@ -# Membership Contributions - Overview - -**Project:** Mila - Membership Management System -**Feature:** Membership Contribution Management -**Version:** 1.0 -**Last Updated:** 2025-11-27 -**Status:** Concept - Ready for Review - ---- - -## Purpose - -This document provides a comprehensive overview of the Membership Contributions system. It covers business logic, data model, UI/UX design, and technical architecture in a concise, bullet-point format. - -**For detailed implementation:** See [contributions-implementation-plan.md](./contributions-implementation-plan.md) (created after concept iterations) - ---- - -## Table of Contents - -1. [Core Principle](#core-principle) -2. [Terminology](#terminology) -3. [Data Model](#data-model) -4. [Business Logic](#business-logic) -5. [UI/UX Design](#uiux-design) -6. [Edge Cases](#edge-cases) -7. [Technical Integration](#technical-integration) -8. [Implementation Scope](#implementation-scope) - ---- - -## Core Principle - -**Maximum Simplicity:** - -- Minimal complexity -- Clear data model without redundancies -- Intuitive operation -- Calendar period-based (Month/Quarter/Half-Year/Year) - ---- - -## Terminology - -### German ↔ English - -**Core Entities:** - -- Beitragsart ↔ Contribution Type / Membership Fee Type -- Beitragsintervall ↔ Contribution Period -- Mitgliedsbeitrag ↔ Membership Fee / Contribution - -**Status:** - -- bezahlt ↔ paid -- unbezahlt ↔ unpaid -- ausgesetzt ↔ suspended / waived - -**Intervals:** - -- monatlich ↔ monthly -- quartalsweise ↔ quarterly -- halbjährlich ↔ half-yearly / semi-annually -- jährlich ↔ yearly / annually - -**UI Elements:** - -- "Letztes Intervall" ↔ "Last Period" (e.g., 2023 when in 2024) -- "Aktuelles Intervall" ↔ "Current Period" (e.g., 2024) -- "Als bezahlt markieren" ↔ "Mark as paid" -- "Aussetzen" ↔ "Suspend" / "Waive" - ---- - -## Data Model - -### Contribution Type (ContributionType) - -``` -- id (UUID) -- name (String) - e.g., "Regular", "Reduced", "Student" -- amount (Decimal) - Contribution amount in Euro -- interval (Enum) - :monthly, :quarterly, :half_yearly, :yearly -- description (Text, optional) -- timestamps -``` - -**Important:** - -- `interval` is **IMMUTABLE** after creation! -- Admin can only change `name`, `amount`, `description` -- On change: Future unpaid periods regenerated with new amount - -### Contribution Period (ContributionPeriod) - -``` -- id (UUID) -- member_id (FK → members.id) -- contribution_type_id (FK → contribution_types.id) -- period_start (Date) - Calendar period start (01.01., 01.04., 01.07., 01.10., etc.) -- status (Enum) - :unpaid (default), :paid, :suspended -- amount (Decimal) - Amount at generation time (history when type changes) -- notes (Text, optional) - Admin notes -- timestamps -``` - -**Important:** - -- **NO** `period_end` - calculated from `period_start` + `interval` -- **NO** `interval_type` - read from `contribution_type.interval` -- Avoids redundancy and inconsistencies! - -**Calendar Period Logic:** - -- Monthly: 01.01. - 31.01., 01.02. - 28./29.02., etc. -- Quarterly: 01.01. - 31.03., 01.04. - 30.06., 01.07. - 30.09., 01.10. - 31.12. -- Half-yearly: 01.01. - 30.06., 01.07. - 31.12. -- Yearly: 01.01. - 31.12. - -### Member (Extensions) - -``` -- contribution_type_id (FK → contribution_types.id, NOT NULL, default from settings) -- contribution_start_date (Date, nullable) - When to start generating contributions -- left_at (Date, nullable) - Exit date (existing) -``` - -**Logic for contribution_start_date:** - -- Auto-set based on global setting `include_joining_period` -- If `include_joining_period = true`: First day of joining month/quarter/year -- If `include_joining_period = false`: First day of NEXT period after joining -- Can be manually overridden by admin - -**NO** `include_joining_period` field on Member - unnecessary due to `contribution_start_date`! - -### Global Settings - -``` -key: "contributions.include_joining_period" -value: Boolean (Default: true) - -key: "contributions.default_contribution_type_id" -value: UUID (Required) - Default contribution type for new members -``` - -**Meaning include_joining_period:** - -- `true`: Joining period is included (member pays from joining period) -- `false`: Only from next full period after joining - -**Meaning default_contribution_type_id:** - -- Every new member automatically gets this contribution type -- Must be configured in admin settings -- Prevents: Members without contribution type - ---- - -## Business Logic - -### Period Generation - -**Triggers:** - -- Member gets contribution type assigned (also during member creation) -- New period begins (Cron job daily/weekly) -- Admin requests manual regeneration - -**Algorithm:** - -1. Get `member.contribution_start_date` and `member.contribution_type` -2. Calculate first period based on `contribution_start_date` -3. Generate all periods from start to today (or `left_at` if present) -4. Skip existing periods -5. Set `amount` to current `contribution_type.amount` - -**Example (Yearly):** - -``` -Joining date: 15.03.2023 -include_joining_period: true -→ contribution_start_date: 01.01.2023 - -Generated periods: -- 01.01.2023 - 31.12.2023 (joining period) -- 01.01.2024 - 31.12.2024 -- 01.01.2025 - 31.12.2025 (current year) -``` - -**Example (Quarterly):** - -``` -Joining date: 15.03.2023 -include_joining_period: false -→ contribution_start_date: 01.04.2023 - -Generated periods: -- 01.04.2023 - 30.06.2023 (first full quarter) -- 01.07.2023 - 30.09.2023 -- 01.10.2023 - 31.12.2023 -- 01.01.2024 - 31.03.2024 -- ... -``` - -### Status Transitions - -``` -unpaid → paid -unpaid → suspended -paid → unpaid -suspended → paid -suspended → unpaid -``` - -**Permissions:** - -- Admin + Treasurer (Kassenwart) can change status -- Uses existing permission system - -### Contribution Type Change - -**MVP - Same Interval Only:** - -- Member can only choose contribution type with **same interval** -- Example: From "Regular (yearly)" to "Reduced (yearly)" ✓ -- Example: From "Regular (yearly)" to "Reduced (monthly)" ✗ - -**Logic on Change:** - -1. Check: New contribution type has same interval -2. If yes: Set `member.contribution_type_id` -3. Future **unpaid** periods: Delete and regenerate with new amount -4. Paid/suspended periods: Remain unchanged (historical amount) - -**Future - Different Intervals:** - -- Enable interval switching (e.g., yearly → monthly) -- More complex logic for period overlaps -- Needs additional validation - -### Member Exit - -**Logic:** - -- Periods only generated until `member.left_at` -- Existing periods remain visible -- Unpaid exit period can be marked as "suspended" - -**Example:** - -``` -Exit: 15.08.2024 -Yearly period: 01.01.2024 - 31.12.2024 - -→ Period 2024 is shown (Status: unpaid) -→ Admin can set to "suspended" -→ No periods for 2025+ generated -``` - ---- - -## UI/UX Design - -### Member List View - -**New Column: "Contribution Status"** - -**Default Display (Last Period):** - -- Shows status of **last completed** period -- Example in 2024: Shows contribution for 2023 -- Color coding: - - Green: paid ✓ - - Red: unpaid ✗ - - Gray: suspended ⊘ - -**Optional: Show Current Period** - -- Toggle: "Show current period" (2024) -- Admin decides what to display - -**Filters:** - -- "Unpaid contributions in last period" -- "Unpaid contributions in current period" - -### Member Detail View - -**Section: "Contributions"** - -**Contribution Type Assignment:** - -``` -┌─────────────────────────────────────┐ -│ Contribution Type: [Dropdown] │ -│ ⚠ Only types with same interval │ -│ can be selected │ -└─────────────────────────────────────┘ -``` - -**Period Table:** - -``` -┌───────────────┬──────────┬────────┬──────────┬─────────┐ -│ Period │ Interval │ Amount │ Status │ Action │ -├───────────────┼──────────┼────────┼──────────┼─────────┤ -│ 01.01.2023- │ Yearly │ 50 € │ ☑ Paid │ │ -│ 31.12.2023 │ │ │ │ │ -├───────────────┼──────────┼────────┼──────────┼─────────┤ -│ 01.01.2024- │ Yearly │ 60 € │ ☐ Open │ [Mark │ -│ 31.12.2024 │ │ │ │ as paid]│ -├───────────────┼──────────┼────────┼──────────┼─────────┤ -│ 01.01.2025- │ Yearly │ 60 € │ ☐ Open │ [Mark │ -│ 31.12.2025 │ │ │ │ as paid]│ -└───────────────┴──────────┴────────┴──────────┴─────────┘ - -Legend: ☑ = paid | ☐ = unpaid | ⊘ = suspended -``` - -**Quick Marking:** - -- Checkbox in each row for fast marking -- Button: "Mark selected as paid/unpaid/suspended" -- Bulk action for multiple periods - -### Admin: Contribution Types Management - -**List:** - -``` -┌────────────┬──────────┬──────────┬────────────┬─────────┐ -│ Name │ Amount │ Interval │ Members │ Actions │ -├────────────┼──────────┼──────────┼────────────┼─────────┤ -│ Regular │ 60 € │ Yearly │ 45 │ [Edit] │ -│ Reduced │ 30 € │ Yearly │ 12 │ [Edit] │ -│ Student │ 20 € │ Monthly │ 8 │ [Edit] │ -└────────────┴──────────┴──────────┴────────────┴─────────┘ -``` - -**Edit:** - -- Name: ✓ editable -- Amount: ✓ editable -- Description: ✓ editable -- Interval: ✗ **NOT** editable (grayed out) - -**Warning on Amount Change:** - -``` -⚠ Change amount to 65 €? - -Impact: -- 45 members affected -- Future unpaid periods will be generated with 65 € -- Already paid periods remain with old amount - -[Cancel] [Confirm] -``` - -### Admin: Settings - -**Contribution Configuration:** - -``` -Default Contribution Type: [Dropdown: Contribution Types] - -Selected: "Regular (60 €, Yearly)" - -This contribution type is automatically assigned to all new members. -Can be changed individually per member. - ---- - -☐ Include joining period - -When active: -Members pay from the period of their joining. - -Example (Yearly): -Joining: 15.03.2023 -→ Pays from 2023 - -When inactive: -Members pay from the next full period. - -Example (Yearly): -Joining: 15.03.2023 -→ Pays from 2024 -``` - ---- - -## Edge Cases - -### 1. Contribution Type Change with Different Interval - -**MVP:** Blocked (only same interval allowed) - -**UI:** - -``` -Error: Interval change not possible - -Current contribution type: "Regular (Yearly)" -Selected contribution type: "Student (Monthly)" - -Changing the interval is currently not possible. -Please select a contribution type with interval "Yearly". - -[OK] -``` - -**Future:** - -- Allow interval switching -- Calculate overlaps -- Generate new periods without duplicates - -### 2. Exit with Unpaid Contributions - -**Scenario:** - -``` -Member exits: 15.08.2024 -Yearly period 2024: unpaid -``` - -**UI Notice on Exit: (Low Prio)** - -``` -⚠ Unpaid contributions present - -This member has 1 unpaid period(s): -- 2024: 60 € (unpaid) - -Do you want to continue? - -[ ] Mark contribution as "suspended" -[Cancel] [Confirm Exit] -``` - -### 3. Multiple Unpaid Periods - -**Scenario:** Member hasn't paid for 2 years - -**Display:** - -``` -┌───────────────┬──────────┬────────┬──────────┬─────────┐ -│ 2023 │ Yearly │ 50 € │ ☐ Open │ [✓] │ -│ 2024 │ Yearly │ 60 € │ ☐ Open │ [✓] │ -│ 2025 │ Yearly │ 60 € │ ☐ Open │ [ ] │ -└───────────────┴──────────┴────────┴──────────┴─────────┘ - -[Mark selected as paid/unpaid/suspended] (2 selected) -``` - -### 4. Amount Changes - -**Scenario:** - -``` -2023: Regular = 50 € -2024: Regular = 60 € (increase) -``` - -**Result:** - -- Period 2023: Saved with 50 € (history) -- Period 2024: Generated with 60 € (current) -- Both periods show correct historical amount - -### 5. Date Boundaries - -**Problem:** What if today = 01.01.2025? - -**Solution:** - -- Current period (2025) is generated -- Status: unpaid (open) -- Shown in overview - ---- - -## Implementation Scope - -### MVP (Phase 1) - -**Included:** - -- ✓ Contribution types (CRUD) -- ✓ Automatic period generation -- ✓ Status management (paid/unpaid/suspended) -- ✓ Member overview with contribution status -- ✓ Period view per member -- ✓ Quick checkbox marking -- ✓ Bulk actions -- ✓ Amount history -- ✓ Same-interval type change -- ✓ Default contribution type -- ✓ Joining period configuration - -**NOT Included:** - -- ✗ Interval change (only same interval) -- ✗ Payment details (date, method) -- ✗ Automatic integration (vereinfacht.digital) -- ✗ Prorata calculation -- ✗ Reports/statistics -- ✗ Reminders/dunning (manual via filters) - -### Future Enhancements - -**Phase 2:** - -- Payment details (date, amount, method) -- Interval change for future unpaid periods -- Manual vereinfacht.digital links per member -- Extended filter options - -**Phase 3:** - -- Automated vereinfacht.digital integration -- Automatic payment matching -- SEPA integration -- Advanced reports diff --git a/docs/feature-roadmap.md b/docs/feature-roadmap.md index 2f86f5e..609523c 100644 --- a/docs/feature-roadmap.md +++ b/docs/feature-roadmap.md @@ -187,16 +187,10 @@ **Current State:** - ✅ Basic "paid" boolean field on members -- ✅ **UI Mock-ups for Contribution Types & Settings** (2025-12-02) - ⚠️ No payment tracking **Open Issues:** - [#156](https://git.local-it.org/local-it/mitgliederverwaltung/issues/156) - Set up & document testing environment for vereinfacht.digital (L, Low priority) -- [#226](https://git.local-it.org/local-it/mitgliederverwaltung/issues/226) - Payment/Contribution Mockup Pages (Preview) - -**Mock-Up Pages (Non-Functional Preview):** -- `/contribution_types` - Contribution Types Management -- `/contribution_settings` - Global Contribution Settings **Missing Features:** - ❌ Membership fee configuration diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index 08133b5..54a5a64 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -79,7 +79,7 @@ defmodule MvWeb.CoreComponents do

{msg}

-
@@ -368,63 +368,61 @@ defmodule MvWeb.CoreComponents do end ~H""" -
- - - - - - - - - - - - + + + +
{col[:label]} - <.live_component - module={MvWeb.Components.SortHeaderComponent} - id={:"sort_custom_field_#{dyn_col[:custom_field].id}"} - field={"custom_field_#{dyn_col[:custom_field].id}"} - label={dyn_col[:custom_field].name} - sort_field={@sort_field} - sort_order={@sort_order} - /> - - {gettext("Actions")} -
- {render_slot(col, @row_item.(row))} - - {if dyn_col[:render] do - rendered = dyn_col[:render].(@row_item.(row)) + + + + + + + + + + + + - - - -
{col[:label]} + <.live_component + module={MvWeb.Components.SortHeaderComponent} + id={:"sort_custom_field_#{dyn_col[:custom_field].id}"} + field={"custom_field_#{dyn_col[:custom_field].id}"} + label={dyn_col[:custom_field].name} + sort_field={@sort_field} + sort_order={@sort_order} + /> + + {gettext("Actions")} +
+ {render_slot(col, @row_item.(row))} + + {if dyn_col[:render] do + rendered = dyn_col[:render].(@row_item.(row)) - if rendered == "" do - "" - else - rendered - end - else + if rendered == "" do "" - end} - -
- <%= for action <- @action do %> - {render_slot(action, @row_item.(row))} - <% end %> -
-
- + else + rendered + end + else + "" + end} +
+
+ <%= for action <- @action do %> + {render_slot(action, @row_item.(row))} + <% end %> +
+
""" end diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex index 1c1138e..7ff7f25 100644 --- a/lib/mv_web/components/layouts/navbar.ex +++ b/lib/mv_web/components/layouts/navbar.ex @@ -25,17 +25,6 @@ defmodule MvWeb.Layouts.Navbar do
  • <.link navigate="/members">{gettext("Members")}
  • <.link navigate="/custom_fields">{gettext("Custom Fields")}
  • <.link navigate="/users">{gettext("Users")}
  • -
  • -
    - {gettext("Contributions")} -
      -
    • <.link navigate="/contribution_types">{gettext("Contribution Types")}
    • -
    • - <.link navigate="/contribution_settings">{gettext("Contribution Settings")} -
    • -
    -
    -
  • diff --git a/lib/mv_web/helpers/date_formatter.ex b/lib/mv_web/helpers/date_formatter.ex deleted file mode 100644 index eaa9271..0000000 --- a/lib/mv_web/helpers/date_formatter.ex +++ /dev/null @@ -1,27 +0,0 @@ -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 diff --git a/lib/mv_web/live/components/sort_header_component.ex b/lib/mv_web/live/components/sort_header_component.ex index 3817d90..b847308 100644 --- a/lib/mv_web/live/components/sort_header_component.ex +++ b/lib/mv_web/live/components/sort_header_component.ex @@ -19,7 +19,7 @@ defmodule MvWeb.Components.SortHeaderComponent do @impl true def render(assigns) do ~H""" -
    +
    - - -
    - - <%!-- Periods Table --%> -
    - - - - - - - - - - - - - - - - - - - - - - - -
    - - {gettext("Time Period")}{gettext("Interval")}{gettext("Amount")}{gettext("Status")}{gettext("Notes")}{gettext("Actions")}
    - - -
    - {period.period_start} – {period.period_end} -
    -
    - {gettext("Current")} -
    -
    - {format_interval(period.interval)} - - {format_currency(period.amount)} - - <.status_badge status={period.status} /> - - - {period.notes} - - - -
    - <.link - href="#" - class={[ - "cursor-not-allowed", - if(period.status == :paid, do: "invisible", else: "opacity-50") - ]} - > - {gettext("Paid")} - - <.link - href="#" - class={[ - "cursor-not-allowed", - if(period.status == :suspended, do: "invisible", else: "opacity-50") - ]} - > - {gettext("Suspend")} - - <.link - href="#" - class={[ - "cursor-not-allowed", - if(period.status != :paid, do: "invisible", else: "opacity-50") - ]} - > - {gettext("Reopen")} - - <.link href="#" class="opacity-50 cursor-not-allowed"> - {gettext("Note")} - -
    -
    -
    - - """ - end - - # Mock-up warning banner component - subtle orange style - defp mockup_warning(assigns) do - ~H""" -
    - <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" /> -
    - {gettext("Preview Mockup")} - - – {gettext("This page is not functional and only displays the planned features.")} - -
    -
    - """ - end - - # Status badge component - attr :status, :atom, required: true - - defp status_badge(%{status: :paid} = assigns) do - ~H""" - - <.icon name="hero-check-circle-mini" class="size-3" /> - {gettext("Paid")} - - """ - end - - defp status_badge(%{status: :unpaid} = assigns) do - ~H""" - - <.icon name="hero-x-circle-mini" class="size-3" /> - {gettext("Unpaid")} - - """ - end - - defp status_badge(%{status: :suspended} = assigns) do - ~H""" - - <.icon name="hero-pause-circle-mini" class="size-3" /> - {gettext("Suspended")} - - """ - 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 diff --git a/lib/mv_web/live/contribution_settings_live.ex b/lib/mv_web/live/contribution_settings_live.ex deleted file mode 100644 index 713bc8c..0000000 --- a/lib/mv_web/live/contribution_settings_live.ex +++ /dev/null @@ -1,277 +0,0 @@ -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""" - - <.mockup_warning /> - - <.header> - {gettext("Contribution Settings")} - <:subtitle> - {gettext("Configure global settings for membership contributions.")} - - - -
    - <%!-- Settings Form --%> -
    -
    -

    - <.icon name="hero-cog-6-tooth" class="size-5" /> - {gettext("Global Settings")} -

    - -
    - <%!-- Default Contribution Type --%> -
    - - -

    - {gettext( - "This contribution type is automatically assigned to all new members. Can be changed individually per member." - )} -

    -
    - - <%!-- Include Joining Period --%> -
    - -
    -

    - {gettext("When active: Members pay from the period of their joining.")} -

    -

    - {gettext("When inactive: Members pay from the next full period after joining.")} -

    -
    -
    - -
    - - -
    -
    -
    - - <%!-- Examples Card --%> -
    -
    -

    - <.icon name="hero-light-bulb" class="size-5" /> - {gettext("Examples")} -

    - - <.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")} - /> - -
    - - <.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")} - /> - -
    - - <.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")} - /> - -
    - - <.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")} - /> -
    -
    -
    - - <.example_member_card /> -
    - """ - end - - # Example member card with link to period view - defp example_member_card(assigns) do - ~H""" -
    -
    -

    - <.icon name="hero-user" class="size-5" /> - {gettext("Example: Member Contribution View")} -

    -

    - {gettext( - "See how the contribution periods will be displayed for an individual member. This example shows Maria Weber with multiple contribution periods." - )} -

    -
    - <.link navigate={~p"/contributions/member/example"} class="btn btn-primary btn-sm"> - <.icon name="hero-eye" class="size-4" /> - {gettext("View Example Member")} - -
    -
    -
    - """ - end - - # Mock-up warning banner component - subtle orange style - defp mockup_warning(assigns) do - ~H""" -
    - <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" /> -
    - {gettext("Preview Mockup")} - - – {gettext("This page is not functional and only displays the planned features.")} - -
    -
    - """ - 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""" -
    -

    {@title}

    -
    -

    - {gettext("Joining date")}: - {@joining_date} -

    -

    - {gettext("Contribution start")}: - {@start_date} -

    -

    - {gettext("Generated periods")}: - - {Enum.join(@periods, ", ")} - -

    -
    -

    → {@note}

    -
    - """ - 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 diff --git a/lib/mv_web/live/contribution_type_live/index.ex b/lib/mv_web/live/contribution_type_live/index.ex deleted file mode 100644 index 9a7b602..0000000 --- a/lib/mv_web/live/contribution_type_live/index.ex +++ /dev/null @@ -1,205 +0,0 @@ -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""" - - <.mockup_warning /> - - <.header> - {gettext("Contribution Types")} - <:subtitle> - {gettext("Manage contribution types for membership fees.")} - - <:actions> - - - - - <.table id="contribution_types" rows={@contribution_types} row_id={fn ct -> "ct-#{ct.id}" end}> - <:col :let={ct} label={gettext("Name")}> - {ct.name} -

    {ct.description}

    - - - <:col :let={ct} label={gettext("Amount")}> - {format_currency(ct.amount)} - - - <:col :let={ct} label={gettext("Interval")}> - {format_interval(ct.interval)} - - - <:col :let={ct} label={gettext("Members")}> - {ct.member_count} - - - <:action :let={_ct}> - - - - <:action :let={ct}> - - - - - <.info_card /> -
    - """ - end - - # Mock-up warning banner component - subtle orange style - defp mockup_warning(assigns) do - ~H""" -
    - <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" /> -
    - {gettext("Preview Mockup")} - - – {gettext("This page is not functional and only displays the planned features.")} - -
    -
    - """ - end - - # Info card explaining the contribution type concept - defp info_card(assigns) do - ~H""" -
    -
    -

    - <.icon name="hero-information-circle" class="size-5" /> - {gettext("About Contribution Types")} -

    -
    -

    - {gettext( - "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." - )} -

    -
      -
    • - {gettext("Name & Amount")} - - {gettext("Can be changed at any time. Amount changes affect future periods only.")} -
    • -
    • - {gettext("Interval")} - - {gettext( - "Fixed after creation. Members can only switch between types with the same interval." - )} -
    • -
    • - {gettext("Deletion")} - - {gettext("Only possible if no members are assigned to this type.")} -
    • -
    -
    -
    -
    - """ - end - - # Mock data for demonstration - defp mock_contribution_types do - [ - %{ - id: "1", - name: gettext("Regular"), - description: gettext("Standard membership fee for regular members"), - amount: Decimal.new("60.00"), - interval: :yearly, - member_count: 45 - }, - %{ - id: "2", - name: gettext("Reduced"), - description: gettext("Reduced fee for unemployed, pensioners, or low income"), - amount: Decimal.new("30.00"), - interval: :yearly, - member_count: 12 - }, - %{ - id: "3", - name: gettext("Student"), - description: gettext("Monthly fee for students and trainees"), - amount: Decimal.new("5.00"), - interval: :monthly, - member_count: 8 - }, - %{ - id: "4", - name: gettext("Family"), - description: gettext("Quarterly fee for family memberships"), - amount: Decimal.new("25.00"), - interval: :quarterly, - member_count: 15 - }, - %{ - id: "5", - name: gettext("Supporting Member"), - description: gettext("Half-yearly contribution for supporting members"), - amount: Decimal.new("100.00"), - interval: :half_yearly, - member_count: 3 - }, - %{ - id: "6", - name: gettext("Honorary"), - description: gettext("No fee for honorary members"), - amount: Decimal.new("0.00"), - interval: :yearly, - member_count: 2 - } - ] - end - - defp format_currency(%Decimal{} = amount) do - "#{Decimal.to_string(amount)} €" - end - - defp format_interval(:monthly), do: gettext("Monthly") - defp format_interval(:quarterly), do: gettext("Quarterly") - defp format_interval(:half_yearly), do: gettext("Half-yearly") - defp format_interval(:yearly), do: gettext("Yearly") -end diff --git a/lib/mv_web/live/member_live/index.ex b/lib/mv_web/live/member_live/index.ex index 792466c..3d30d76 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -32,7 +32,6 @@ 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_") @custom_field_prefix "custom_field_" @@ -138,7 +137,13 @@ defmodule MvWeb.MemberLive.Index do selected_ids = socket.assigns.selected_members # Filter members that are in the selection and have email addresses - formatted_emails = format_selected_member_emails(socket.assigns.members, selected_ids) + formatted_emails = + socket.assigns.members + |> Enum.filter(fn member -> + MapSet.member?(selected_ids, member.id) && member.email && member.email != "" + end) + |> Enum.map(&format_member_email/1) + email_count = length(formatted_emails) cond do @@ -882,20 +887,9 @@ defmodule MvWeb.MemberLive.Index do end end - # Filters selected members with email addresses and formats them. - # Returns a list of formatted email strings in the format "First Last ". - # Used by both copy_emails and mailto links. - def format_selected_member_emails(members, selected_members) do - members - |> Enum.filter(fn member -> - MapSet.member?(selected_members, member.id) && member.email && member.email != "" - end) - |> Enum.map(&format_member_email/1) - end - # Formats a member's email in the format "First Last " - # Used for copy_emails feature and mailto links to create email-client-friendly format. - def format_member_email(member) do + # Used for copy_emails feature to create email-client-friendly format. + defp format_member_email(member) do first_name = member.first_name || "" last_name = member.last_name || "" @@ -938,7 +932,4 @@ 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 diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 959a3bc..58e22b6 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -14,12 +14,7 @@ <.button :if={Enum.any?(@members, &MapSet.member?(@selected_members, &1.id))} - href={ - "mailto:?bcc=" <> - (MvWeb.MemberLive.Index.format_selected_member_emails(@members, @selected_members) - |> Enum.join(", ") - |> URI.encode()) - } + href={"mailto:?bcc=#{@members |> Enum.filter(&(MapSet.member?(@selected_members, &1.id) && &1.email)) |> Enum.map(& &1.email) |> Enum.join(",")}"} aria-label={gettext("Open email program with BCC recipients")} > <.icon name="hero-envelope" /> @@ -224,7 +219,7 @@ """ } > - {MvWeb.MemberLive.Index.format_date(member.join_date)} + {member.join_date} <:col :let={member} label={gettext("Paid")}> DateFormatter.format_date(date) + {:ok, date} -> Date.to_string(date) _ -> value end end diff --git a/lib/mv_web/live/member_live/show.ex b/lib/mv_web/live/member_live/show.ex index 7601f46..de46a3a 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -23,7 +23,6 @@ defmodule MvWeb.MemberLive.Show do """ use MvWeb, :live_view import Ash.Query - alias MvWeb.Helpers.DateFormatter @impl true def render(assigns) do @@ -53,8 +52,8 @@ defmodule MvWeb.MemberLive.Show do {if @member.paid, do: gettext("Yes"), else: gettext("No")} <:item title={gettext("Phone Number")}>{@member.phone_number} - <:item title={gettext("Join Date")}>{DateFormatter.format_date(@member.join_date)} - <:item title={gettext("Exit Date")}>{DateFormatter.format_date(@member.exit_date)} + <:item title={gettext("Join Date")}>{@member.join_date} + <:item title={gettext("Exit Date")}>{@member.exit_date} <:item title={gettext("Notes")}>{@member.notes} <:item title={gettext("City")}>{@member.city} <:item title={gettext("Street")}>{@member.street} @@ -82,7 +81,10 @@ defmodule MvWeb.MemberLive.Show do # name cfv.custom_field && cfv.custom_field.name, # value - format_custom_field_value(cfv) + case cfv.value do + %{value: v} -> v + v -> v + end } end) } /> @@ -112,17 +114,4 @@ defmodule MvWeb.MemberLive.Show do defp page_title(:show), do: gettext("Show Member") defp page_title(:edit), do: gettext("Edit Member") - - defp format_custom_field_value(cfv) do - value = - case cfv.value do - %{value: v} -> v - v -> v - end - - case value do - %Date{} = date -> DateFormatter.format_date(date) - other -> other - end - end end diff --git a/lib/mv_web/live/user_live/form.ex b/lib/mv_web/live/user_live/form.ex index 0639e75..9619a15 100644 --- a/lib/mv_web/live/user_live/form.ex +++ b/lib/mv_web/live/user_live/form.ex @@ -42,7 +42,7 @@ defmodule MvWeb.UserLive.Form do <:subtitle>{gettext("Use this form to manage user records in your database.")} - <.form class="max-w-xl" for={@form} id="user-form" phx-change="validate" phx-submit="save"> + <.form for={@form} id="user-form" phx-change="validate" phx-submit="save"> <.input field={@form[:email]} label={gettext("Email")} required type="email" /> @@ -61,7 +61,7 @@ defmodule MvWeb.UserLive.Form do <%= if @show_password_fields do %> -
    +
    <.input field={@form[:password]} label={gettext("Password")} @@ -83,7 +83,7 @@ defmodule MvWeb.UserLive.Form do

    {gettext("Password requirements")}:

    -
      +
      • {gettext("At least 8 characters")}
      • {gettext("Include both letters and numbers")}
      • {gettext("Consider using special characters")}
      • @@ -91,7 +91,7 @@ defmodule MvWeb.UserLive.Form do
    <%= if @user do %> -
    +

    {gettext("Admin Note")}: {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

    <% else %> <%= if @user do %> -
    +

    {gettext("Note")}: {gettext( "Check 'Change Password' above to set a new password for this user." @@ -110,7 +110,7 @@ defmodule MvWeb.UserLive.Form do

    <% else %> -
    +

    {gettext("Note")}: {gettext( "User will be created without a password. Check 'Set Password' to add one." @@ -123,11 +123,11 @@ defmodule MvWeb.UserLive.Form do

    -

    {gettext("Linked Member")}

    +

    {gettext("Linked Member")}

    <%= if @user && @user.member && !@unlink_member do %> -
    +

    @@ -147,7 +147,7 @@ defmodule MvWeb.UserLive.Form do <% else %> <%= if @unlink_member do %> -

    +

    {gettext("Unlinking scheduled")}: {gettext( "Member will be unlinked when you save. Cannot select new member until saved." @@ -219,7 +219,7 @@ defmodule MvWeb.UserLive.Form do

    <%= if @user && @user.email && @available_members != [] && Enum.all?(@available_members, &(&1.email == to_string(@user.email))) do %> -
    +

    {gettext("Note")}: {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 %>

    {gettext("Selected")}: {@selected_member_name}

    -

    +

    {gettext("Save to confirm linking.")}

    @@ -245,12 +245,10 @@ defmodule MvWeb.UserLive.Form do <% end %>
    -
    - <.button phx-disable-with={gettext("Saving...")} variant="primary"> - {gettext("Save User")} - - <.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")} -
    + <.button phx-disable-with={gettext("Saving...")} variant="primary"> + {gettext("Save User")} + + <.button navigate={return_path(@return_to, @user)}>{gettext("Cancel")} """ diff --git a/lib/mv_web/live/user_live/index.html.heex b/lib/mv_web/live/user_live/index.html.heex index 9a98159..3582046 100644 --- a/lib/mv_web/live/user_live/index.html.heex +++ b/lib/mv_web/live/user_live/index.html.heex @@ -49,6 +49,7 @@ > {user.email} + <:col :let={user} label={gettext("OIDC ID")}>{user.oidc_id} <:col :let={user} label={gettext("Linked Member")}> <%= if user.member do %> {user.member.first_name} {user.member.last_name} diff --git a/lib/mv_web/live/user_live/show.ex b/lib/mv_web/live/user_live/show.ex index 777def1..664f99f 100644 --- a/lib/mv_web/live/user_live/show.ex +++ b/lib/mv_web/live/user_live/show.ex @@ -46,7 +46,9 @@ defmodule MvWeb.UserLive.Show do <.list> + <:item title={gettext("ID")}>{@user.id} <:item title={gettext("Email")}>{@user.email} + <:item title={gettext("OIDC ID")}>{@user.oidc_id || gettext("Not set")} <:item title={gettext("Password Authentication")}> {if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")} @@ -54,13 +56,13 @@ defmodule MvWeb.UserLive.Show do <%= if @user.member do %> <.link navigate={~p"/members/#{@user.member}"} - class="text-blue-600 underline hover:text-blue-800" + class="text-blue-600 hover:text-blue-800 underline" > - <.icon name="hero-users" class="inline w-4 h-4 mr-1" /> + <.icon name="hero-users" class="h-4 w-4 inline mr-1" /> {@user.member.first_name} {@user.member.last_name} <% else %> - {gettext("No member linked")} + {gettext("No member linked")} <% end %> diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex index c574e17..09a2792 100644 --- a/lib/mv_web/router.ex +++ b/lib/mv_web/router.ex @@ -75,11 +75,6 @@ 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 diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index be7ffef..7a76f62 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -11,12 +11,11 @@ msgstr "" "Language: en\n" #: lib/mv_web/components/core_components.ex:386 -#: 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/member_live/index.html.heex:243 #: lib/mv_web/live/user_live/index.html.heex:72 #, elixir-autogen, elixir-format msgid "Are you sure?" @@ -29,26 +28,24 @@ msgid "Attempting to reconnect" msgstr "Verbindung wird wiederhergestellt" #: lib/mv_web/live/member_live/form.ex:53 -#: lib/mv_web/live/member_live/index.html.heex:184 +#: lib/mv_web/live/member_live/index.html.heex:179 #: lib/mv_web/live/member_live/show.ex:58 #, elixir-autogen, elixir-format 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/member_live/index.html.heex:245 #: lib/mv_web/live/user_live/index.html.heex:74 #, elixir-autogen, elixir-format msgid "Delete" msgstr "Löschen" -#: lib/mv_web/live/contribution_type_live/index.ex:66 -#: lib/mv_web/live/member_live/index.html.heex:242 +#: lib/mv_web/live/member_live/index.html.heex:237 #: lib/mv_web/live/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 #, elixir-autogen, elixir-format msgid "Edit" -msgstr "Bearbeiten" +msgstr "Bearbeite" #: lib/mv_web/live/member_live/show.ex:41 #: lib/mv_web/live/member_live/show.ex:116 @@ -56,9 +53,8 @@ msgstr "Bearbeiten" msgid "Edit Member" msgstr "Mitglied bearbeiten" -#: lib/mv_web/live/contribution_period_live/show.ex:58 #: lib/mv_web/live/member_live/form.ex:47 -#: lib/mv_web/live/member_live/index.html.heex:112 +#: lib/mv_web/live/member_live/index.html.heex:107 #: lib/mv_web/live/member_live/show.ex:50 #: lib/mv_web/live/user_live/form.ex:46 #: lib/mv_web/live/user_live/index.html.heex:44 @@ -74,7 +70,7 @@ msgid "First Name" msgstr "Vorname" #: lib/mv_web/live/member_live/form.ex:50 -#: lib/mv_web/live/member_live/index.html.heex:220 +#: lib/mv_web/live/member_live/index.html.heex:215 #: lib/mv_web/live/member_live/show.ex:55 #, elixir-autogen, elixir-format msgid "Join Date" @@ -86,12 +82,12 @@ msgstr "Beitrittsdatum" msgid "Last Name" msgstr "Nachname" -#: lib/mv_web/live/member_live/index.html.heex:29 +#: lib/mv_web/live/member_live/index.html.heex:24 #, elixir-autogen, elixir-format msgid "New Member" msgstr "Neues Mitglied" -#: lib/mv_web/live/member_live/index.html.heex:239 +#: lib/mv_web/live/member_live/index.html.heex:234 #: lib/mv_web/live/user_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Show" @@ -119,13 +115,12 @@ msgid "Exit Date" msgstr "Austrittsdatum" #: lib/mv_web/live/member_live/form.ex:55 -#: lib/mv_web/live/member_live/index.html.heex:148 +#: lib/mv_web/live/member_live/index.html.heex:143 #: lib/mv_web/live/member_live/show.ex:60 #, elixir-autogen, elixir-format msgid "House Number" msgstr "Hausnummer" -#: lib/mv_web/live/contribution_period_live/show.ex:140 #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/member_live/show.ex:57 #, elixir-autogen, elixir-format @@ -134,24 +129,22 @@ 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:242 #: lib/mv_web/live/member_live/form.ex:48 -#: lib/mv_web/live/member_live/index.html.heex:229 +#: lib/mv_web/live/member_live/index.html.heex:224 #: lib/mv_web/live/member_live/show.ex:51 #, elixir-autogen, elixir-format msgid "Paid" msgstr "Bezahlt" #: lib/mv_web/live/member_live/form.ex:49 -#: lib/mv_web/live/member_live/index.html.heex:202 +#: lib/mv_web/live/member_live/index.html.heex:197 #: lib/mv_web/live/member_live/show.ex:54 #, elixir-autogen, elixir-format msgid "Phone Number" msgstr "Telefonnummer" #: lib/mv_web/live/member_live/form.ex:56 -#: lib/mv_web/live/member_live/index.html.heex:166 +#: lib/mv_web/live/member_live/index.html.heex:161 #: lib/mv_web/live/member_live/show.ex:61 #, elixir-autogen, elixir-format msgid "Postal Code" @@ -172,7 +165,7 @@ msgid "Saving..." msgstr "Speichern..." #: lib/mv_web/live/member_live/form.ex:54 -#: lib/mv_web/live/member_live/index.html.heex:130 +#: lib/mv_web/live/member_live/index.html.heex:125 #: lib/mv_web/live/member_live/show.ex:59 #, elixir-autogen, elixir-format msgid "Street" @@ -183,7 +176,7 @@ msgstr "Straße" msgid "Id" msgstr "ID" -#: lib/mv_web/live/member_live/index.html.heex:234 +#: lib/mv_web/live/member_live/index.html.heex:229 #: lib/mv_web/live/member_live/index/formatter.ex:61 #: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format @@ -200,7 +193,7 @@ msgstr "Mitglied anzeigen" msgid "This is a member record from your database." msgstr "Dies ist ein Mitglied aus deiner Datenbank." -#: lib/mv_web/live/member_live/index.html.heex:234 +#: lib/mv_web/live/member_live/index.html.heex:229 #: lib/mv_web/live/member_live/index/formatter.ex:60 #: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format @@ -295,7 +288,7 @@ msgstr "ID" msgid "Immutable" msgstr "Unveränderlich" -#: lib/mv_web/components/layouts/navbar.ex:113 +#: lib/mv_web/components/layouts/navbar.ex:102 #, elixir-autogen, elixir-format msgid "Logout" msgstr "Abmelden" @@ -312,14 +305,12 @@ msgid "Member" msgstr "Mitglied" #: lib/mv_web/components/layouts/navbar.ex:25 -#: lib/mv_web/live/contribution_type_live/index.ex:61 #: lib/mv_web/live/member_live/index.ex:73 #: 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" @@ -340,7 +331,6 @@ msgstr "Nicht aktiviert" 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 @@ -359,7 +349,7 @@ msgstr "OIDC ID" msgid "Password Authentication" msgstr "Passwort-Authentifizierung" -#: lib/mv_web/components/layouts/navbar.ex:106 +#: lib/mv_web/components/layouts/navbar.ex:95 #, elixir-autogen, elixir-format msgid "Profil" msgstr "Profil" @@ -369,17 +359,17 @@ msgstr "Profil" msgid "Required" msgstr "Erforderlich" -#: lib/mv_web/live/member_live/index.html.heex:68 +#: lib/mv_web/live/member_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Select all members" msgstr "Alle Mitglieder auswählen" -#: lib/mv_web/live/member_live/index.html.heex:82 +#: lib/mv_web/live/member_live/index.html.heex:77 #, elixir-autogen, elixir-format msgid "Select member" msgstr "Mitglied auswählen" -#: lib/mv_web/components/layouts/navbar.ex:110 +#: lib/mv_web/components/layouts/navbar.ex:99 #, elixir-autogen, elixir-format msgid "Settings" msgstr "Einstellungen" @@ -547,20 +537,20 @@ msgstr "Zurück zur Mitgliederliste" msgid "Back to users list" msgstr "Zurück zur Benutzer*innen-Liste" -#: lib/mv_web/components/layouts/navbar.ex:44 -#: lib/mv_web/components/layouts/navbar.ex:50 +#: lib/mv_web/components/layouts/navbar.ex:33 +#: lib/mv_web/components/layouts/navbar.ex:39 #, elixir-autogen, elixir-format msgid "Select language" msgstr "Sprache auswählen" -#: lib/mv_web/components/layouts/navbar.ex:57 -#: lib/mv_web/components/layouts/navbar.ex:77 +#: lib/mv_web/components/layouts/navbar.ex:46 +#: lib/mv_web/components/layouts/navbar.ex:66 #, elixir-autogen, elixir-format msgid "Toggle dark mode" msgstr "Dunklen Modus umschalten" #: lib/mv_web/live/components/search_bar_component.ex:15 -#: lib/mv_web/live/member_live/index.html.heex:39 +#: lib/mv_web/live/member_live/index.html.heex:34 #, elixir-autogen, elixir-format msgid "Search..." msgstr "Suchen..." @@ -576,7 +566,7 @@ msgstr "Benutzer*innen" msgid "Click to sort" msgstr "Klicke um zu sortieren" -#: lib/mv_web/live/member_live/index.html.heex:94 +#: lib/mv_web/live/member_live/index.html.heex:89 #, elixir-autogen, elixir-format msgid "First name" msgstr "Vorname" @@ -726,7 +716,6 @@ 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" @@ -787,7 +776,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:165 #, elixir-autogen, elixir-format msgid "Copied %{count} email address to clipboard" msgid_plural "Copied %{count} email addresses to clipboard" @@ -804,27 +793,27 @@ 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:154 #, 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:151 #, elixir-autogen, elixir-format msgid "No members selected" msgstr "Keine Mitglieder ausgewählt" -#: lib/mv_web/live/member_live/index.html.heex:23 +#: lib/mv_web/live/member_live/index.html.heex:18 #, elixir-autogen, elixir-format msgid "Open email program with BCC recipients" msgstr "E-Mail-Programm mit BCC-Empfänger*innen öffnen" -#: lib/mv_web/live/member_live/index.html.heex:26 +#: lib/mv_web/live/member_live/index.html.heex:21 #, elixir-autogen, elixir-format 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:174 #, 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" @@ -864,432 +853,6 @@ msgstr "Nicht bezahlt" msgid "Payment filter" msgstr "Zahlungsfilter" -#: 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] "%{count} Beiträge ausgewählt" -msgstr[1] "%{count} Beiträge ausgewählt" - -#: lib/mv_web/live/contribution_type_live/index.ex:113 -#, elixir-autogen, elixir-format -msgid "About Contribution Types" -msgstr "Über Beitragsarten" - -#: 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 "Betrag" - -#: lib/mv_web/live/contribution_period_live/show.ex:48 -#, elixir-autogen, elixir-format -msgid "Back to Settings" -msgstr "Zurück zu Einstellungen" - -#: 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 "Kann jederzeit geändert werden. Betragsänderungen wirken sich nur auf zukünftige Beiträge aus." - -#: lib/mv_web/live/contribution_type_live/index.ex:77 -#, elixir-autogen, elixir-format -msgid "Cannot delete - members assigned" -msgstr "Löschen nicht möglich - Mitglieder zugewiesen" - -#: lib/mv_web/live/contribution_period_live/show.ex:83 -#, elixir-autogen, elixir-format -msgid "Change Contribution Type" -msgstr "Beitragsart ändern" - -#: lib/mv_web/live/contribution_settings_live.ex:42 -#, elixir-autogen, elixir-format -msgid "Configure global settings for membership contributions." -msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren." - -#: 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 "Beitragseinstellungen" - -#: lib/mv_web/live/contribution_period_live/show.ex:62 -#, elixir-autogen, elixir-format -msgid "Contribution Start" -msgstr "Beitragsbeginn" - -#: 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 "Beitragsarten" - -#: lib/mv_web/live/contribution_settings_live.ex:224 -#, elixir-autogen, elixir-format -msgid "Contribution start" -msgstr "Beitragsbeginn" - -#: lib/mv_web/live/contribution_period_live/show.ex:41 -#, elixir-autogen, elixir-format -msgid "Contribution type" -msgstr "Beitragsart" - -#: 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 "Beitragsarten definieren verschiedene Mitgliedsbeitragsstrukturen. Jede Art hat ein festes Intervall (monatlich, quartalsweise, halbjährlich, jährlich), das nach der Erstellung nicht mehr geändert werden kann." - -#: lib/mv_web/components/layouts/navbar.ex:30 -#, elixir-autogen, elixir-format -msgid "Contributions" -msgstr "Beiträge" - -#: lib/mv_web/live/contribution_period_live/show.ex:39 -#, elixir-autogen, elixir-format -msgid "Contributions for %{name}" -msgstr "Beiträge für %{name}" - -#: lib/mv_web/live/contribution_period_live/show.ex:159 -#, elixir-autogen, elixir-format -msgid "Current" -msgstr "Aktuell" - -#: lib/mv_web/live/contribution_settings_live.ex:60 -#, elixir-autogen, elixir-format -msgid "Default Contribution Type" -msgstr "Standard-Beitragsart" - -#: lib/mv_web/live/contribution_type_live/index.ex:133 -#, elixir-autogen, elixir-format -msgid "Deletion" -msgstr "Löschung" - -#: lib/mv_web/live/contribution_settings_live.ex:173 -#, elixir-autogen, elixir-format -msgid "Example: Member Contribution View" -msgstr "Beispiel: Mitglieder-Beitragsansicht" - -#: lib/mv_web/live/contribution_settings_live.ex:113 -#, elixir-autogen, elixir-format -msgid "Examples" -msgstr "Beispiele" - -#: 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 "Familie" - -#: 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 "Nach der Erstellung unveränderlich. Mitglieder können nur zwischen Arten mit demselben Intervall wechseln." - -#: lib/mv_web/live/contribution_settings_live.ex:228 -#, elixir-autogen, elixir-format -msgid "Generated periods" -msgstr "Generierte Beiträge" - -#: lib/mv_web/live/contribution_settings_live.ex:52 -#, elixir-autogen, elixir-format -msgid "Global Settings" -msgstr "Globale Einstellungen" - -#: lib/mv_web/live/contribution_period_live/show.ex:344 -#: 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 "Halbjährlich" - -#: lib/mv_web/live/contribution_type_live/index.ex:181 -#, elixir-autogen, elixir-format -msgid "Half-yearly contribution for supporting members" -msgstr "Halbjährlicher Beitrag für Fördermitglieder" - -#: 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 "Ehrenmitglied" - -#: lib/mv_web/live/contribution_settings_live.ex:85 -#, elixir-autogen, elixir-format -msgid "Include joining period" -msgstr "Zahlt ab Zeitpunkt des Eintritts" - -#: 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 "Intervall" - -#: lib/mv_web/live/contribution_settings_live.ex:220 -#, elixir-autogen, elixir-format -msgid "Joining date" -msgstr "Eintrittsdatum" - -#: lib/mv_web/live/contribution_period_live/show.ex:332 -#, elixir-autogen, elixir-format -msgid "Joining year - reduced to 0" -msgstr "Eintrittsjahr - auf 0 reduziert" - -#: lib/mv_web/live/contribution_type_live/index.ex:38 -#, elixir-autogen, elixir-format -msgid "Manage contribution types for membership fees." -msgstr "Beitragsarten für Mitgliedsbeiträge verwalten." - -#: lib/mv_web/live/contribution_period_live/show.ex:116 -#, elixir-autogen, elixir-format -msgid "Mark as Paid" -msgstr "Als bezahlt markieren" - -#: lib/mv_web/live/contribution_period_live/show.ex:120 -#, elixir-autogen, elixir-format -msgid "Mark as Suspended" -msgstr "Als ausgesetzt markieren" - -#: lib/mv_web/live/contribution_period_live/show.ex:124 -#, elixir-autogen, elixir-format -msgid "Mark as Unpaid" -msgstr "Als unbezahlt markieren" - -#: lib/mv_web/live/contribution_period_live/show.ex:26 -#, elixir-autogen, elixir-format -msgid "Member Contributions" -msgstr "Mitgliedsbeiträge" - -#: lib/mv_web/live/contribution_settings_live.ex:122 -#, elixir-autogen, elixir-format -msgid "Member pays for the year they joined" -msgstr "Mitglied zahlt für das Eintrittsjahr" - -#: lib/mv_web/live/contribution_settings_live.ex:155 -#, elixir-autogen, elixir-format -msgid "Member pays from the joining month" -msgstr "Mitglied zahlt ab dem Eintrittsmonat" - -#: lib/mv_web/live/contribution_settings_live.ex:144 -#, elixir-autogen, elixir-format -msgid "Member pays from the next full quarter" -msgstr "Mitglied zahlt ab dem nächsten vollen Quartal" - -#: lib/mv_web/live/contribution_settings_live.ex:133 -#, elixir-autogen, elixir-format -msgid "Member pays from the next full year" -msgstr "Mitglied zahlt ab dem nächsten vollen Jahr" - -#: lib/mv_web/live/contribution_period_live/show.ex:43 -#, elixir-autogen, elixir-format -msgid "Member since" -msgstr "Mitglied seit" - -#: 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 "Mitglieder können nur zwischen Beitragsarten mit demselben Zahlungsintervall wechseln (z.B. jährlich zu jährlich). Dies verhindert komplexe Periodenüberschneidungen." - -#: lib/mv_web/live/contribution_period_live/show.ex:342 -#: 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 "Monatlich" - -#: lib/mv_web/live/contribution_settings_live.ex:150 -#, elixir-autogen, elixir-format -msgid "Monthly Interval - Joining Period Included" -msgstr "Monatliches Intervall - Eintrittsperiode eingeschlossen" - -#: lib/mv_web/live/contribution_type_live/index.ex:165 -#, elixir-autogen, elixir-format -msgid "Monthly fee for students and trainees" -msgstr "Monatlicher Beitrag für Studierende und Auszubildende" - -#: lib/mv_web/live/contribution_type_live/index.ex:123 -#, elixir-autogen, elixir-format -msgid "Name & Amount" -msgstr "Name & Betrag" - -#: lib/mv_web/live/contribution_type_live/index.ex:42 -#, elixir-autogen, elixir-format -msgid "New Contribution Type" -msgstr "Neue Beitragsart" - -#: lib/mv_web/live/contribution_type_live/index.ex:189 -#, elixir-autogen, elixir-format -msgid "No fee for honorary members" -msgstr "Kein Beitrag für Ehrenmitglieder" - -#: 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 "Nur möglich, wenn keine Mitglieder dieser Art zugewiesen sind." - -#: lib/mv_web/live/contribution_period_live/show.ex:70 -#, elixir-autogen, elixir-format -msgid "Open Contributions" -msgstr "Offene Beiträge" - -#: lib/mv_web/live/contribution_period_live/show.ex:302 -#, elixir-autogen, elixir-format -msgid "Paid via bank transfer" -msgstr "Per Überweisung bezahlt" - -#: lib/mv_web/live/contribution_period_live/show.ex:226 -#: 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 "Vorschau" - -#: lib/mv_web/live/contribution_period_live/show.ex:343 -#: 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 "Quartalsweise" - -#: lib/mv_web/live/contribution_settings_live.ex:139 -#, elixir-autogen, elixir-format -msgid "Quarterly Interval - Joining Period Excluded" -msgstr "Quartalsintervall - Eintrittsperiode ausgeschlossen" - -#: lib/mv_web/live/contribution_type_live/index.ex:173 -#, elixir-autogen, elixir-format -msgid "Quarterly fee for family memberships" -msgstr "Quartalsbeitrag für Familienmitgliedschaften" - -#: 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 "Ermäßigt" - -#: lib/mv_web/live/contribution_type_live/index.ex:157 -#, elixir-autogen, elixir-format -msgid "Reduced fee for unemployed, pensioners, or low income" -msgstr "Ermäßigter Beitrag für Arbeitslose, Rentner*innen oder Geringverdienende" - -#: lib/mv_web/live/contribution_period_live/show.ex:276 -#: 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 "Regulär" - -#: lib/mv_web/live/contribution_period_live/show.ex:204 -#, elixir-autogen, elixir-format -msgid "Reopen" -msgstr "Wieder öffnen" - -#: 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 "Sehen Sie, wie die Beitragsperioden für ein einzelnes Mitglied angezeigt werden. Dieses Beispiel zeigt Maria Weber mit mehreren Beitragsperioden." - -#: lib/mv_web/live/contribution_type_live/index.ex:149 -#, elixir-autogen, elixir-format -msgid "Standard membership fee for regular members" -msgstr "Standard-Mitgliedsbeitrag für reguläre Mitglieder" - -#: lib/mv_web/live/contribution_period_live/show.ex:139 -#, elixir-autogen, elixir-format -msgid "Status" -msgstr "Status" - -#: 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 "Student*in" - -#: lib/mv_web/live/contribution_type_live/index.ex:180 -#, elixir-autogen, elixir-format -msgid "Supporting Member" -msgstr "Fördermitglied" - -#: lib/mv_web/live/contribution_period_live/show.ex:195 -#, elixir-autogen, elixir-format -msgid "Suspend" -msgstr "Aussetzen" - -#: lib/mv_web/live/contribution_period_live/show.ex:260 -#, elixir-autogen, elixir-format -msgid "Suspended" -msgstr "Ausgesetzt" - -#: 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 "Diese Beitragsart wird automatisch allen neuen Mitgliedern zugewiesen. Kann pro Mitglied individuell geändert werden." - -#: lib/mv_web/live/contribution_period_live/show.ex:228 -#: 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 "Diese Seite ist nicht funktional und zeigt nur die geplanten Funktionen." - -#: lib/mv_web/live/contribution_period_live/show.ex:136 -#, elixir-autogen, elixir-format -msgid "Time Period" -msgstr "Zeitraum" - -#: lib/mv_web/live/contribution_period_live/show.ex:66 -#, elixir-autogen, elixir-format -msgid "Total Contributions" -msgstr "Beiträge gesamt" - -#: lib/mv_web/live/contribution_period_live/show.ex:251 -#, elixir-autogen, elixir-format -msgid "Unpaid" -msgstr "Unbezahlt" - -#: lib/mv_web/live/contribution_settings_live.ex:183 -#, elixir-autogen, elixir-format -msgid "View Example Member" -msgstr "Beispielmitglied ansehen" - -#: 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 "Wenn aktiv: Mitglieder zahlen ab der Periode ihres Eintritts." - -#: 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 "Wenn inaktiv: Mitglieder zahlen ab der nächsten vollen Periode nach dem Eintritt." - -#: lib/mv_web/live/contribution_period_live/show.ex:98 -#, elixir-autogen, elixir-format -msgid "Why are not all contribution types shown?" -msgstr "Warum werden nicht alle Beitragsarten angezeigt?" - -#: 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:345 -#: 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 "Jährlich" - -#: lib/mv_web/live/contribution_settings_live.ex:128 -#, elixir-autogen, elixir-format -msgid "Yearly Interval - Joining Period Excluded" -msgstr "Jährliches Intervall - Eintrittsperiode ausgeschlossen" - -#: lib/mv_web/live/contribution_settings_live.ex:117 -#, elixir-autogen, elixir-format -msgid "Yearly Interval - Joining Period Included" -msgstr "Jährliches Intervall - Eintrittsperiode eingeschlossen" - #~ #: lib/mv_web/live/member_live/form.ex:48 #~ #: lib/mv_web/live/member_live/show.ex:51 #~ #, elixir-autogen, elixir-format diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 8594746..7229e28 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -12,12 +12,11 @@ msgid "" msgstr "" #: lib/mv_web/components/core_components.ex:386 -#: 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/member_live/index.html.heex:243 #: lib/mv_web/live/user_live/index.html.heex:72 #, elixir-autogen, elixir-format msgid "Are you sure?" @@ -30,21 +29,19 @@ msgid "Attempting to reconnect" msgstr "" #: lib/mv_web/live/member_live/form.ex:53 -#: lib/mv_web/live/member_live/index.html.heex:184 +#: lib/mv_web/live/member_live/index.html.heex:179 #: lib/mv_web/live/member_live/show.ex:58 #, elixir-autogen, elixir-format 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/member_live/index.html.heex:245 #: lib/mv_web/live/user_live/index.html.heex:74 #, elixir-autogen, elixir-format msgid "Delete" msgstr "" -#: lib/mv_web/live/contribution_type_live/index.ex:66 -#: lib/mv_web/live/member_live/index.html.heex:242 +#: lib/mv_web/live/member_live/index.html.heex:237 #: lib/mv_web/live/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 #, elixir-autogen, elixir-format @@ -57,9 +54,8 @@ msgstr "" msgid "Edit Member" msgstr "" -#: lib/mv_web/live/contribution_period_live/show.ex:58 #: lib/mv_web/live/member_live/form.ex:47 -#: lib/mv_web/live/member_live/index.html.heex:112 +#: lib/mv_web/live/member_live/index.html.heex:107 #: lib/mv_web/live/member_live/show.ex:50 #: lib/mv_web/live/user_live/form.ex:46 #: lib/mv_web/live/user_live/index.html.heex:44 @@ -75,7 +71,7 @@ msgid "First Name" msgstr "" #: lib/mv_web/live/member_live/form.ex:50 -#: lib/mv_web/live/member_live/index.html.heex:220 +#: lib/mv_web/live/member_live/index.html.heex:215 #: lib/mv_web/live/member_live/show.ex:55 #, elixir-autogen, elixir-format msgid "Join Date" @@ -87,12 +83,12 @@ msgstr "" msgid "Last Name" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:29 +#: lib/mv_web/live/member_live/index.html.heex:24 #, elixir-autogen, elixir-format msgid "New Member" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:239 +#: lib/mv_web/live/member_live/index.html.heex:234 #: lib/mv_web/live/user_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Show" @@ -120,13 +116,12 @@ msgid "Exit Date" msgstr "" #: lib/mv_web/live/member_live/form.ex:55 -#: lib/mv_web/live/member_live/index.html.heex:148 +#: lib/mv_web/live/member_live/index.html.heex:143 #: lib/mv_web/live/member_live/show.ex:60 #, elixir-autogen, elixir-format msgid "House Number" msgstr "" -#: lib/mv_web/live/contribution_period_live/show.ex:140 #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/member_live/show.ex:57 #, elixir-autogen, elixir-format @@ -135,24 +130,22 @@ 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:242 #: lib/mv_web/live/member_live/form.ex:48 -#: lib/mv_web/live/member_live/index.html.heex:229 +#: lib/mv_web/live/member_live/index.html.heex:224 #: lib/mv_web/live/member_live/show.ex:51 #, elixir-autogen, elixir-format msgid "Paid" msgstr "" #: lib/mv_web/live/member_live/form.ex:49 -#: lib/mv_web/live/member_live/index.html.heex:202 +#: lib/mv_web/live/member_live/index.html.heex:197 #: lib/mv_web/live/member_live/show.ex:54 #, elixir-autogen, elixir-format msgid "Phone Number" msgstr "" #: lib/mv_web/live/member_live/form.ex:56 -#: lib/mv_web/live/member_live/index.html.heex:166 +#: lib/mv_web/live/member_live/index.html.heex:161 #: lib/mv_web/live/member_live/show.ex:61 #, elixir-autogen, elixir-format msgid "Postal Code" @@ -173,7 +166,7 @@ msgid "Saving..." msgstr "" #: lib/mv_web/live/member_live/form.ex:54 -#: lib/mv_web/live/member_live/index.html.heex:130 +#: lib/mv_web/live/member_live/index.html.heex:125 #: lib/mv_web/live/member_live/show.ex:59 #, elixir-autogen, elixir-format msgid "Street" @@ -184,7 +177,7 @@ msgstr "" msgid "Id" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:234 +#: lib/mv_web/live/member_live/index.html.heex:229 #: lib/mv_web/live/member_live/index/formatter.ex:61 #: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format @@ -201,7 +194,7 @@ msgstr "" msgid "This is a member record from your database." msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:234 +#: lib/mv_web/live/member_live/index.html.heex:229 #: lib/mv_web/live/member_live/index/formatter.ex:60 #: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format @@ -296,7 +289,7 @@ msgstr "" msgid "Immutable" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:113 +#: lib/mv_web/components/layouts/navbar.ex:102 #, elixir-autogen, elixir-format msgid "Logout" msgstr "" @@ -313,14 +306,12 @@ msgid "Member" msgstr "" #: lib/mv_web/components/layouts/navbar.ex:25 -#: lib/mv_web/live/contribution_type_live/index.ex:61 #: lib/mv_web/live/member_live/index.ex:73 #: 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" @@ -341,7 +332,6 @@ msgstr "" 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 @@ -360,7 +350,7 @@ msgstr "" msgid "Password Authentication" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:106 +#: lib/mv_web/components/layouts/navbar.ex:95 #, elixir-autogen, elixir-format msgid "Profil" msgstr "" @@ -370,17 +360,17 @@ msgstr "" msgid "Required" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:68 +#: lib/mv_web/live/member_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Select all members" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:82 +#: lib/mv_web/live/member_live/index.html.heex:77 #, elixir-autogen, elixir-format msgid "Select member" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:110 +#: lib/mv_web/components/layouts/navbar.ex:99 #, elixir-autogen, elixir-format msgid "Settings" msgstr "" @@ -548,20 +538,20 @@ msgstr "" msgid "Back to users list" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:44 -#: lib/mv_web/components/layouts/navbar.ex:50 +#: lib/mv_web/components/layouts/navbar.ex:33 +#: lib/mv_web/components/layouts/navbar.ex:39 #, elixir-autogen, elixir-format msgid "Select language" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:57 -#: lib/mv_web/components/layouts/navbar.ex:77 +#: lib/mv_web/components/layouts/navbar.ex:46 +#: lib/mv_web/components/layouts/navbar.ex:66 #, elixir-autogen, elixir-format msgid "Toggle dark mode" msgstr "" #: lib/mv_web/live/components/search_bar_component.ex:15 -#: lib/mv_web/live/member_live/index.html.heex:39 +#: lib/mv_web/live/member_live/index.html.heex:34 #, elixir-autogen, elixir-format msgid "Search..." msgstr "" @@ -577,7 +567,7 @@ msgstr "" msgid "Click to sort" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:94 +#: lib/mv_web/live/member_live/index.html.heex:89 #, elixir-autogen, elixir-format msgid "First name" msgstr "" @@ -727,7 +717,6 @@ 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" @@ -788,7 +777,7 @@ msgstr "" msgid "Unlinking scheduled" msgstr "" -#: lib/mv_web/live/member_live/index.ex:159 +#: lib/mv_web/live/member_live/index.ex:165 #, elixir-autogen, elixir-format msgid "Copied %{count} email address to clipboard" msgid_plural "Copied %{count} email addresses to clipboard" @@ -805,27 +794,27 @@ msgstr "" msgid "Copy emails" msgstr "" -#: lib/mv_web/live/member_live/index.ex:148 +#: lib/mv_web/live/member_live/index.ex:154 #, 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:151 #, elixir-autogen, elixir-format msgid "No members selected" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:23 +#: lib/mv_web/live/member_live/index.html.heex:18 #, elixir-autogen, elixir-format msgid "Open email program with BCC recipients" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:26 +#: lib/mv_web/live/member_live/index.html.heex:21 #, elixir-autogen, elixir-format msgid "Open in email program" msgstr "" -#: lib/mv_web/live/member_live/index.ex:168 +#: lib/mv_web/live/member_live/index.ex:174 #, elixir-autogen, elixir-format msgid "Tip: Paste email addresses into the BCC field for privacy compliance" msgstr "" @@ -864,429 +853,3 @@ msgstr "" #, elixir-autogen, elixir-format msgid "Payment filter" 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:344 -#: 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:332 -#, 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:342 -#: 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:302 -#, elixir-autogen, elixir-format -msgid "Paid via bank transfer" -msgstr "" - -#: lib/mv_web/live/contribution_period_live/show.ex:226 -#: 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:343 -#: 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:276 -#: 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:260 -#, 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:228 -#: 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:251 -#, 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:345 -#: 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 "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index cf081bf..3b471d5 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -12,12 +12,11 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: lib/mv_web/components/core_components.ex:386 -#: 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/member_live/index.html.heex:243 #: lib/mv_web/live/user_live/index.html.heex:72 #, elixir-autogen, elixir-format msgid "Are you sure?" @@ -30,21 +29,19 @@ msgid "Attempting to reconnect" msgstr "" #: lib/mv_web/live/member_live/form.ex:53 -#: lib/mv_web/live/member_live/index.html.heex:184 +#: lib/mv_web/live/member_live/index.html.heex:179 #: lib/mv_web/live/member_live/show.ex:58 #, elixir-autogen, elixir-format 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/member_live/index.html.heex:245 #: lib/mv_web/live/user_live/index.html.heex:74 #, elixir-autogen, elixir-format msgid "Delete" msgstr "" -#: lib/mv_web/live/contribution_type_live/index.ex:66 -#: lib/mv_web/live/member_live/index.html.heex:242 +#: lib/mv_web/live/member_live/index.html.heex:237 #: lib/mv_web/live/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 #, elixir-autogen, elixir-format @@ -57,9 +54,8 @@ msgstr "" msgid "Edit Member" msgstr "" -#: lib/mv_web/live/contribution_period_live/show.ex:58 #: lib/mv_web/live/member_live/form.ex:47 -#: lib/mv_web/live/member_live/index.html.heex:112 +#: lib/mv_web/live/member_live/index.html.heex:107 #: lib/mv_web/live/member_live/show.ex:50 #: lib/mv_web/live/user_live/form.ex:46 #: lib/mv_web/live/user_live/index.html.heex:44 @@ -75,7 +71,7 @@ msgid "First Name" msgstr "" #: lib/mv_web/live/member_live/form.ex:50 -#: lib/mv_web/live/member_live/index.html.heex:220 +#: lib/mv_web/live/member_live/index.html.heex:215 #: lib/mv_web/live/member_live/show.ex:55 #, elixir-autogen, elixir-format msgid "Join Date" @@ -87,12 +83,12 @@ msgstr "" msgid "Last Name" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:29 +#: lib/mv_web/live/member_live/index.html.heex:24 #, elixir-autogen, elixir-format msgid "New Member" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:239 +#: lib/mv_web/live/member_live/index.html.heex:234 #: lib/mv_web/live/user_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Show" @@ -120,13 +116,12 @@ msgid "Exit Date" msgstr "" #: lib/mv_web/live/member_live/form.ex:55 -#: lib/mv_web/live/member_live/index.html.heex:148 +#: lib/mv_web/live/member_live/index.html.heex:143 #: lib/mv_web/live/member_live/show.ex:60 #, elixir-autogen, elixir-format msgid "House Number" msgstr "" -#: lib/mv_web/live/contribution_period_live/show.ex:140 #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/member_live/show.ex:57 #, elixir-autogen, elixir-format @@ -135,24 +130,22 @@ 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:242 #: lib/mv_web/live/member_live/form.ex:48 -#: lib/mv_web/live/member_live/index.html.heex:229 +#: lib/mv_web/live/member_live/index.html.heex:224 #: lib/mv_web/live/member_live/show.ex:51 #, elixir-autogen, elixir-format msgid "Paid" msgstr "" #: lib/mv_web/live/member_live/form.ex:49 -#: lib/mv_web/live/member_live/index.html.heex:202 +#: lib/mv_web/live/member_live/index.html.heex:197 #: lib/mv_web/live/member_live/show.ex:54 #, elixir-autogen, elixir-format msgid "Phone Number" msgstr "" #: lib/mv_web/live/member_live/form.ex:56 -#: lib/mv_web/live/member_live/index.html.heex:166 +#: lib/mv_web/live/member_live/index.html.heex:161 #: lib/mv_web/live/member_live/show.ex:61 #, elixir-autogen, elixir-format msgid "Postal Code" @@ -173,7 +166,7 @@ msgid "Saving..." msgstr "" #: lib/mv_web/live/member_live/form.ex:54 -#: lib/mv_web/live/member_live/index.html.heex:130 +#: lib/mv_web/live/member_live/index.html.heex:125 #: lib/mv_web/live/member_live/show.ex:59 #, elixir-autogen, elixir-format msgid "Street" @@ -184,7 +177,7 @@ msgstr "" msgid "Id" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:234 +#: lib/mv_web/live/member_live/index.html.heex:229 #: lib/mv_web/live/member_live/index/formatter.ex:61 #: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format @@ -201,7 +194,7 @@ msgstr "" msgid "This is a member record from your database." msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:234 +#: lib/mv_web/live/member_live/index.html.heex:229 #: lib/mv_web/live/member_live/index/formatter.ex:60 #: lib/mv_web/live/member_live/show.ex:52 #, elixir-autogen, elixir-format @@ -296,7 +289,7 @@ msgstr "" msgid "Immutable" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:113 +#: lib/mv_web/components/layouts/navbar.ex:102 #, elixir-autogen, elixir-format msgid "Logout" msgstr "" @@ -313,14 +306,12 @@ msgid "Member" msgstr "" #: lib/mv_web/components/layouts/navbar.ex:25 -#: lib/mv_web/live/contribution_type_live/index.ex:61 #: lib/mv_web/live/member_live/index.ex:73 #: 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" @@ -341,7 +332,6 @@ msgstr "" 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 @@ -360,7 +350,7 @@ msgstr "" msgid "Password Authentication" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:106 +#: lib/mv_web/components/layouts/navbar.ex:95 #, elixir-autogen, elixir-format msgid "Profil" msgstr "" @@ -370,17 +360,17 @@ msgstr "" msgid "Required" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:68 +#: lib/mv_web/live/member_live/index.html.heex:63 #, elixir-autogen, elixir-format msgid "Select all members" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:82 +#: lib/mv_web/live/member_live/index.html.heex:77 #, elixir-autogen, elixir-format msgid "Select member" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:110 +#: lib/mv_web/components/layouts/navbar.ex:99 #, elixir-autogen, elixir-format msgid "Settings" msgstr "" @@ -548,20 +538,20 @@ msgstr "" msgid "Back to users list" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:44 -#: lib/mv_web/components/layouts/navbar.ex:50 +#: lib/mv_web/components/layouts/navbar.ex:33 +#: lib/mv_web/components/layouts/navbar.ex:39 #, elixir-autogen, elixir-format, fuzzy msgid "Select language" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:57 -#: lib/mv_web/components/layouts/navbar.ex:77 +#: lib/mv_web/components/layouts/navbar.ex:46 +#: lib/mv_web/components/layouts/navbar.ex:66 #, elixir-autogen, elixir-format msgid "Toggle dark mode" msgstr "" #: lib/mv_web/live/components/search_bar_component.ex:15 -#: lib/mv_web/live/member_live/index.html.heex:39 +#: lib/mv_web/live/member_live/index.html.heex:34 #, elixir-autogen, elixir-format msgid "Search..." msgstr "" @@ -577,7 +567,7 @@ msgstr "" msgid "Click to sort" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:94 +#: lib/mv_web/live/member_live/index.html.heex:89 #, elixir-autogen, elixir-format, fuzzy msgid "First name" msgstr "" @@ -727,7 +717,6 @@ 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" @@ -788,7 +777,7 @@ msgstr "" msgid "Unlinking scheduled" msgstr "" -#: lib/mv_web/live/member_live/index.ex:159 +#: lib/mv_web/live/member_live/index.ex:165 #, elixir-autogen, elixir-format msgid "Copied %{count} email address to clipboard" msgid_plural "Copied %{count} email addresses to clipboard" @@ -805,27 +794,27 @@ msgstr "" msgid "Copy emails" msgstr "" -#: lib/mv_web/live/member_live/index.ex:148 +#: lib/mv_web/live/member_live/index.ex:154 #, 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:151 #, elixir-autogen, elixir-format, fuzzy msgid "No members selected" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:23 +#: lib/mv_web/live/member_live/index.html.heex:18 #, elixir-autogen, elixir-format msgid "Open email program with BCC recipients" msgstr "" -#: lib/mv_web/live/member_live/index.html.heex:26 +#: lib/mv_web/live/member_live/index.html.heex:21 #, elixir-autogen, elixir-format msgid "Open in email program" msgstr "" -#: lib/mv_web/live/member_live/index.ex:168 +#: lib/mv_web/live/member_live/index.ex:174 #, elixir-autogen, elixir-format msgid "Tip: Paste email addresses into the BCC field for privacy compliance" msgstr "" @@ -865,440 +854,8 @@ msgstr "" msgid "Payment filter" 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, 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:344 -#: 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:332 -#, 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:342 -#: 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:302 -#, elixir-autogen, elixir-format -msgid "Paid via bank transfer" -msgstr "" - -#: lib/mv_web/live/contribution_period_live/show.ex:226 -#: 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:343 -#: 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:276 -#: 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:260 -#, 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:228 -#: 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:251 -#, 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:345 -#: 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 "" - #~ #: lib/mv_web/live/member_live/form.ex:48 #~ #: lib/mv_web/live/member_live/show.ex:51 #~ #, elixir-autogen, elixir-format #~ msgid "Birth Date" #~ msgstr "" - -#~ #: lib/mv_web/live/contribution_period_live/show.ex:273 -#~ #: lib/mv_web/live/contribution_settings_live.ex:248 -#~ #, elixir-autogen, elixir-format -#~ msgid "Related Pages" -#~ msgstr "" diff --git a/test/mv_web/live/profile_navigation_test.exs b/test/mv_web/live/profile_navigation_test.exs index 5ba5eb0..3222825 100644 --- a/test/mv_web/live/profile_navigation_test.exs +++ b/test/mv_web/live/profile_navigation_test.exs @@ -90,6 +90,8 @@ 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 diff --git a/test/mv_web/member_live/index_custom_fields_display_test.exs b/test/mv_web/member_live/index_custom_fields_display_test.exs index 802cc8f..0485f5e 100644 --- a/test/mv_web/member_live/index_custom_fields_display_test.exs +++ b/test/mv_web/member_live/index_custom_fields_display_test.exs @@ -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 European format (dd.mm.yyyy) - assert html =~ "15.05.1990" + # Date should be displayed in readable format + assert html =~ "1990" or html =~ "1990-05-15" or html =~ "15.05.1990" end test "formats email custom field values correctly", %{conn: conn, member1: _member1} do diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs index 360ef72..c0b0275 100644 --- a/test/mv_web/user_live/index_test.exs +++ b/test/mv_web/user_live/index_test.exs @@ -33,6 +33,8 @@ 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 @@ -384,6 +386,10 @@ 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