diff --git a/.gitignore b/.gitignore index 63ff39e..9517a21 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ npm-debug.log .env .elixir_ls/ + +# Docker secrets directory (generated by `just init-secrets`) +/secrets/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d9147..28b4a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CopyToClipboard JavaScript hook with fallback for older browsers - Button shows count of visible selected members (respects search/filter) - German/English translations +- Docker secrets support via `_FILE` environment variables for all sensitive configuration (SECRET_KEY_BASE, TOKEN_SIGNING_SECRET, OIDC_CLIENT_SECRET, DATABASE_URL, DATABASE_PASSWORD) ### Fixed - Email validation false positive when linking user and member with identical emails (#168 Problem #4) diff --git a/Justfile b/Justfile index a91c0e4..25fb35c 100644 --- a/Justfile +++ b/Justfile @@ -90,4 +90,27 @@ clean: remove-gettext-conflicts: #!/usr/bin/env bash set -euo pipefail - find priv/gettext -type f -exec sed -i '/^<<<<<<< HEAD$/d; /^=======$/d; /^>>>>>>>/d' {} \; \ No newline at end of file + 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 diff --git a/README.md b/README.md index 6db7980..14435db 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,13 @@ For testing the production Docker build locally: # OIDC_BASE_URL=http://localhost:8080/auth/v1 # OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback # OIDC_CLIENT_SECRET= + + # Alternative: Use _FILE variables for Docker secrets (takes priority over regular vars): + # SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base + # TOKEN_SIGNING_SECRET_FILE=/run/secrets/token_signing_secret + # OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc_client_secret + # DATABASE_URL_FILE=/run/secrets/database_url + # DATABASE_PASSWORD_FILE=/run/secrets/database_password ``` 3. **Start development environment** (for Rauthy): @@ -250,7 +257,7 @@ For actual production deployment: - Set `OIDC_BASE_URL` to your production OIDC provider - Configure proper Docker networks 3. **Set up SSL/TLS** (e.g., via reverse proxy like Nginx/Traefik) -4. **Use secure secrets management** (environment variables, Docker secrets, vault) +4. **Use secure secrets management** — All sensitive environment variables support a `_FILE` suffix for Docker secrets (e.g., `SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base`). See `docker-compose.prod.yml` for an example setup with Docker secrets. 5. **Configure database backups** diff --git a/config/runtime.exs b/config/runtime.exs index c50356c..71138ef 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -7,6 +7,75 @@ import Config # any compile-time configuration in here, as it won't be applied. # The block below contains prod specific runtime configuration. +# Helper function to read environment variables with Docker secrets support. +# Supports the _FILE suffix pattern: if VAR_FILE is set, reads the value from +# that file path. Otherwise falls back to VAR directly. +# VAR_FILE takes priority and must contain the full absolute path to the secret file. +get_env_or_file = fn var_name, default -> + file_var = "#{var_name}_FILE" + + case System.get_env(file_var) do + nil -> + System.get_env(var_name, default) + + file_path -> + case File.read(file_path) do + {:ok, content} -> + String.trim_trailing(content) + + {:error, reason} -> + raise """ + Failed to read secret from file specified in #{file_var}="#{file_path}". + Error: #{inspect(reason)} + """ + end + end +end + +# Same as get_env_or_file but raises if the value is not set +get_env_or_file! = fn var_name, error_message -> + case get_env_or_file.(var_name, nil) do + nil -> raise error_message + value -> value + end +end + +# Build database URL from individual components or use DATABASE_URL directly. +# Supports both approaches: +# 1. DATABASE_URL (or DATABASE_URL_FILE) - full connection URL +# 2. Separate vars: DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD (or _FILE), DATABASE_NAME, DATABASE_PORT +build_database_url = fn -> + case get_env_or_file.("DATABASE_URL", nil) do + nil -> + # Build URL from separate components + host = + System.get_env("DATABASE_HOST") || + raise "DATABASE_HOST is required when DATABASE_URL is not set" + + user = + System.get_env("DATABASE_USER") || + raise "DATABASE_USER is required when DATABASE_URL is not set" + + password = + get_env_or_file!.("DATABASE_PASSWORD", """ + DATABASE_PASSWORD or DATABASE_PASSWORD_FILE is required when DATABASE_URL is not set. + """) + + database = + System.get_env("DATABASE_NAME") || + raise "DATABASE_NAME is required when DATABASE_URL is not set" + + port = System.get_env("DATABASE_PORT", "5432") + + # URL-encode the password to handle special characters + encoded_password = URI.encode_www_form(password) + "ecto://#{user}:#{encoded_password}@#{host}:#{port}/#{database}" + + url -> + url + end +end + # ## Using releases # # If you use `mix release`, you need to explicitly enable the server @@ -21,12 +90,7 @@ if System.get_env("PHX_SERVER") do end if config_env() == :prod do - database_url = - System.get_env("DATABASE_URL") || - raise """ - environment variable DATABASE_URL is missing. - For example: ecto://USER:PASS@HOST/DATABASE - """ + database_url = build_database_url.() maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] @@ -41,12 +105,12 @@ if config_env() == :prod do # want to use a different value for prod and you most likely don't want # to check this value into version control, so we use an environment # variable instead. + # Supports SECRET_KEY_BASE or SECRET_KEY_BASE_FILE for Docker secrets. secret_key_base = - System.get_env("SECRET_KEY_BASE") || - raise """ - environment variable SECRET_KEY_BASE is missing. - You can generate one by calling: mix phx.gen.secret - """ + get_env_or_file!.("SECRET_KEY_BASE", """ + environment variable SECRET_KEY_BASE (or SECRET_KEY_BASE_FILE) is missing. + You can generate one by calling: mix phx.gen.secret + """) host = System.get_env("PHX_HOST") || raise "Please define the PHX_HOST environment variable." port = String.to_integer(System.get_env("PORT") || "4000") @@ -54,32 +118,47 @@ if config_env() == :prod do config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") # Rauthy OIDC configuration + # Supports OIDC_CLIENT_SECRET or OIDC_CLIENT_SECRET_FILE for Docker secrets. + # OIDC_CLIENT_SECRET is required only if OIDC is being used (indicated by explicit OIDC env vars). + oidc_base_url = System.get_env("OIDC_BASE_URL") + oidc_client_id = System.get_env("OIDC_CLIENT_ID") + oidc_in_use = not is_nil(oidc_base_url) or not is_nil(oidc_client_id) + + client_secret = + if oidc_in_use do + get_env_or_file!.("OIDC_CLIENT_SECRET", """ + environment variable OIDC_CLIENT_SECRET (or OIDC_CLIENT_SECRET_FILE) is missing. + This is required when OIDC authentication is configured (OIDC_BASE_URL or OIDC_CLIENT_ID is set). + """) + else + get_env_or_file.("OIDC_CLIENT_SECRET", nil) + end + config :mv, :rauthy, - client_id: System.get_env("OIDC_CLIENT_ID") || "mv", - base_url: System.get_env("OIDC_BASE_URL") || "http://localhost:8080/auth/v1", - client_secret: System.get_env("OIDC_CLIENT_SECRET"), + client_id: oidc_client_id || "mv", + base_url: oidc_base_url || "http://localhost:8080/auth/v1", + client_secret: client_secret, redirect_uri: System.get_env("OIDC_REDIRECT_URI") || "http://#{host}:#{port}/auth/user/rauthy/callback" # Token signing secret from environment variable # This overrides the placeholder value set in prod.exs + # Supports TOKEN_SIGNING_SECRET or TOKEN_SIGNING_SECRET_FILE for Docker secrets. token_signing_secret = - System.get_env("TOKEN_SIGNING_SECRET") || - raise """ - environment variable TOKEN_SIGNING_SECRET is missing. - You can generate one by calling: mix phx.gen.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 + """) config :mv, :token_signing_secret, token_signing_secret config :mv, MvWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ - # Enable IPv6 and bind on all interfaces. - # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. + # Bind on all IPv4 interfaces. + # Use {0, 0, 0, 0, 0, 0, 0, 0} for IPv6, or {127, 0, 0, 1} for localhost only. # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 - # for details about using IPv6 vs IPv4 and loopback vs public addresses. - ip: {0, 0, 0, 0, 0, 0, 0, 0}, + ip: {0, 0, 0, 0}, port: port ], secret_key_base: secret_key_base, diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0bb2840..b4b7a1f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -2,21 +2,32 @@ services: app: image: git.local-it.org/local-it/mitgliederverwaltung:latest container_name: mv-prod-app - # Use host network for local testing to access localhost:8080 (Rauthy) - # In real production, remove this and use external OIDC provider - network_mode: host + ports: + - "4001:4001" environment: - DATABASE_URL: "ecto://postgres:postgres@localhost:5001/mv_prod" - SECRET_KEY_BASE: "${SECRET_KEY_BASE}" - TOKEN_SIGNING_SECRET: "${TOKEN_SIGNING_SECRET}" - PHX_HOST: "${PHX_HOST}" + # Database configuration using separate variables + # Use Docker service name for internal networking + DATABASE_HOST: "db-prod" + DATABASE_PORT: "5432" + DATABASE_USER: "postgres" + DATABASE_NAME: "mv_prod" + DATABASE_PASSWORD_FILE: "/run/secrets/db_password" + # Phoenix secrets via Docker secrets + SECRET_KEY_BASE_FILE: "/run/secrets/secret_key_base" + TOKEN_SIGNING_SECRET_FILE: "/run/secrets/token_signing_secret" + PHX_HOST: "${PHX_HOST:-localhost}" PORT: "4001" PHX_SERVER: "true" - # Rauthy OIDC config - uses localhost because of host network mode + # Rauthy OIDC config - use host.docker.internal to reach host services OIDC_CLIENT_ID: "mv" - OIDC_BASE_URL: "http://localhost:8080/auth/v1" - OIDC_CLIENT_SECRET: "${OIDC_CLIENT_SECRET:-}" + OIDC_BASE_URL: "http://host.docker.internal:8080/auth/v1" + OIDC_CLIENT_SECRET_FILE: "/run/secrets/oidc_client_secret" OIDC_REDIRECT_URI: "http://localhost:4001/auth/user/rauthy/callback" + secrets: + - db_password + - secret_key_base + - token_signing_secret + - oidc_client_secret depends_on: - db-prod restart: unless-stopped @@ -26,13 +37,25 @@ services: container_name: mv-prod-db environment: POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres + POSTGRES_PASSWORD_FILE: /run/secrets/db_password POSTGRES_DB: mv_prod + secrets: + - db_password volumes: - postgres_data_prod:/var/lib/postgresql/data ports: - "5001:5432" restart: unless-stopped +secrets: + db_password: + file: ./secrets/db_password.txt + secret_key_base: + file: ./secrets/secret_key_base.txt + token_signing_secret: + file: ./secrets/token_signing_secret.txt + oidc_client_secret: + file: ./secrets/oidc_client_secret.txt + volumes: postgres_data_prod: diff --git a/docs/contributions-architecture.md b/docs/contributions-architecture.md new file mode 100644 index 0000000..3718a3b --- /dev/null +++ b/docs/contributions-architecture.md @@ -0,0 +1,653 @@ +# Membership Contributions - Technical Architecture + +**Project:** Mila - Membership Management System +**Feature:** Membership Contribution Management +**Version:** 1.0 +**Last Updated:** 2025-11-27 +**Status:** Architecture Design - Ready for Implementation + +--- + +## Purpose + +This document defines the technical architecture for the Membership Contributions system. It focuses on architectural decisions, patterns, module structure, and integration points **without** concrete implementation details. + +**Related Documents:** +- [contributions-overview.md](./contributions-overview.md) - Business logic and requirements +- [database-schema-readme.md](./database-schema-readme.md) - Database documentation +- [database_schema.dbml](./database_schema.dbml) - Database schema definition + +--- + +## Table of Contents + +1. [Architecture Principles](#architecture-principles) +2. [Domain Structure](#domain-structure) +3. [Data Architecture](#data-architecture) +4. [Business Logic Architecture](#business-logic-architecture) +5. [Integration Points](#integration-points) +6. [Acceptance Criteria](#acceptance-criteria) +7. [Testing Strategy](#testing-strategy) +8. [Security Considerations](#security-considerations) +9. [Performance Considerations](#performance-considerations) + +--- + +## Architecture Principles + +### Core Design Decisions + +1. **Single Responsibility:** + - Each module has one clear responsibility + - Period generation separated from status management + - Calendar logic isolated in dedicated module + +2. **No Redundancy:** + - No `period_end` field (calculated from `period_start` + `interval`) + - No `interval_type` field (read from `contribution_type.interval`) + - Eliminates data inconsistencies + +3. **Immutability Where Important:** + - `contribution_type.interval` cannot be changed after creation + - Prevents complex migration scenarios + - Enforced via Ash change validation + +4. **Historical Accuracy:** + - `amount` stored per period for audit trail + - Enables tracking of contribution changes over time + - Old periods retain original amounts + +5. **Calendar-Based Periods:** + - All periods aligned to calendar boundaries + - Simplifies date calculations + - Predictable period generation + +--- + +## Domain Structure + +### Ash Domain: `Mv.Contributions` + +**Purpose:** Encapsulates all contribution-related resources and logic + +**Resources:** +- `ContributionType` - Contribution type definitions (admin-managed) +- `ContributionPeriod` - Individual contribution periods per member + +**Extensions:** +- Member resource extended with contribution fields + +### Module Organization + +``` +lib/ +├── contributions/ +│ ├── contributions.ex # Ash domain definition +│ ├── contribution_type.ex # ContributionType resource +│ ├── contribution_period.ex # ContributionPeriod resource +│ └── changes/ +│ ├── prevent_interval_change.ex # Validates interval immutability +│ ├── set_contribution_start_date.ex # Auto-sets start date +│ └── validate_same_interval.ex # Validates interval match on type change +├── mv/ +│ └── contributions/ +│ ├── period_generator.ex # Period generation algorithm +│ └── calendar_periods.ex # Calendar period calculations +└── membership/ + └── member.ex # Extended with contribution relationships +``` + +### Separation of Concerns + +**Domain Layer (Ash Resources):** +- Data validation +- Relationship management +- Policy enforcement +- Action definitions + +**Business Logic Layer (`Mv.Contributions`):** +- Period generation algorithm +- Calendar calculations +- Date boundary handling +- Status transitions + +**UI Layer (LiveView):** +- User interaction +- Display logic +- Authorization checks +- Form handling + +--- + +## Data Architecture + +### Database Schema Extensions + +**See:** [database-schema-readme.md](./database-schema-readme.md) and [database_schema.dbml](./database_schema.dbml) for complete schema documentation. + +### New Tables + +1. **`contribution_types`** + - Purpose: Define contribution types with fixed intervals + - Key Constraint: `interval` field immutable after creation + - Relationships: has_many members, has_many contribution_periods + +2. **`contribution_periods`** + - Purpose: Individual contribution periods for members + - Key Design: NO `period_end` or `interval_type` fields (calculated) + - Relationships: belongs_to member, belongs_to contribution_type + - Composite uniqueness: One period per member per period_start + +### Member Table Extensions + +**Fields Added:** +- `contribution_type_id` (FK, NOT NULL with default from settings) +- `contribution_start_date` (Date, nullable) + +**Existing Fields Used:** +- `joined_at` - For calculating contribution start +- `left_at` - For limiting period generation +- These fields must remain member fields and should not be replaced by custom fields in the future + +### Settings Integration + +**Global Settings:** +- `contributions.include_joining_period` (Boolean) +- `contributions.default_contribution_type_id` (UUID) + +**Storage:** Existing settings mechanism (TBD: dedicated table or configuration resource) + +### Foreign Key Behaviors + +| Relationship | On Delete | Rationale | +|--------------|-----------|-----------| +| `contribution_periods.member_id → members.id` | CASCADE | Remove periods when member deleted | +| `contribution_periods.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if periods exist | +| `members.contribution_type_id → contribution_types.id` | RESTRICT | Prevent type deletion if assigned to members | + +--- + +## Business Logic Architecture + +### Period Generation System + +**Component:** `Mv.Contributions.PeriodGenerator` + +**Responsibilities:** +- Calculate which periods should exist for a member +- Generate missing periods +- Respect contribution_start_date and left_at boundaries +- Skip existing periods (idempotent) + +**Triggers:** +1. Member contribution type assigned (via Ash change) +2. Member created with contribution type (via Ash change) +3. Scheduled job runs (daily/weekly cron) +4. Admin manual regeneration (UI action) + +**Algorithm Steps:** +1. Retrieve member with contribution_type and dates +2. Determine first period start (based on contribution_start_date) +3. Calculate all period starts from first to today (or left_at) +4. Query existing periods for member +5. Generate missing periods with current contribution_type.amount +6. Insert new periods (batch operation) + +**Edge Case Handling:** +- If contribution_start_date is NULL: Calculate from joined_at + global setting +- If left_at is set: Stop generation at left_at +- If contribution_type changes: Handled separately by regeneration logic + +### Calendar Period Calculations + +**Component:** `Mv.Contributions.CalendarPeriods` + +**Responsibilities:** +- Calculate period boundaries based on interval type +- Determine current period +- Determine last completed period +- Calculate period_end from period_start + interval + +**Functions (high-level):** +- `calculate_period_start/3` - Given date and interval, find period start +- `calculate_period_end/2` - Given period_start and interval, calculate end +- `next_period_start/2` - Given period_start and interval, find next +- `is_current_period?/2` - Check if period contains today +- `is_last_completed_period?/2` - Check if period just ended + +**Interval Logic:** +- **Monthly:** Start = 1st of month, End = last day of month +- **Quarterly:** Start = 1st of quarter (Jan/Apr/Jul/Oct), End = last day of quarter +- **Half-yearly:** Start = 1st of half (Jan/Jul), End = last day of half +- **Yearly:** Start = Jan 1st, End = Dec 31st + +### Status Management + +**Component:** Ash actions on `ContributionPeriod` + +**Status Transitions:** +- Simple state machine: unpaid ↔ paid ↔ suspended +- No complex validation (all transitions allowed) +- Permissions checked via Ash policies + +**Actions Required:** +- `mark_as_paid` - Set status to :paid +- `mark_as_suspended` - Set status to :suspended +- `mark_as_unpaid` - Set status to :unpaid (error correction) + +**Bulk Operations:** +- `bulk_mark_as_paid` - Mark multiple periods as paid (efficiency) + - low priority, can be a future issue + +### Contribution Type Change Handling + +**Component:** Ash change on `Member.contribution_type_id` + +**Validation:** +- Check if new type has same interval as old type +- If different: Reject change (MVP constraint) +- If same: Allow change + +**Side Effects on Allowed Change:** +1. Keep all existing periods unchanged +2. Find future unpaid periods +3. Delete future unpaid periods +4. Regenerate periods with new contribution_type_id and amount + +**Implementation Pattern:** +- Use Ash change module to validate +- Use after_action hook to trigger regeneration +- Use transaction to ensure atomicity + +--- + +## Integration Points + +### Member Resource Integration + +**Extension Points:** +1. Add fields via migration +2. Add relationships (belongs_to, has_many) +3. Add calculations (current_period_status, overdue_count) +4. Add changes (auto-set contribution_start_date, validate interval) + +**Backward Compatibility:** +- New fields nullable or with defaults +- Existing members get default contribution type from settings +- No breaking changes to existing member functionality + +### Settings System Integration + +**Requirements:** +- Store two global settings +- Provide UI for admin to modify +- Default values if not set +- Validation (e.g., default_contribution_type_id must exist) + +**Access Pattern:** +- Read settings during period generation +- Read settings during member creation +- Write settings only via admin UI + +### Permission System Integration + +**See:** [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md) + +**Required Permissions:** +- `ContributionType.create/update/destroy` - Admin only +- `ContributionType.read` - Admin, Treasurer, Board +- `ContributionPeriod.update` (status changes) - Admin, Treasurer +- `ContributionPeriod.read` - Admin, Treasurer, Board, Own member + +**Policy Patterns:** +- Use existing HasPermission check +- Leverage existing roles (Admin, Kassenwart) +- Member can read own periods (linked via member_id) + +### LiveView Integration + +**New LiveViews Required:** +1. ContributionType index/form (admin) +2. ContributionPeriod table component (member detail view) +3. Settings form section (admin) +4. Member list column (contribution status) + +**Existing LiveViews to Extend:** +- Member detail view: Add contributions section +- Member list view: Add status column +- Settings page: Add contributions section + +**Authorization Helpers:** +- Use existing `can?/3` helper for UI conditionals +- Check permissions before showing actions + +--- + +## Acceptance Criteria + +### ContributionType Resource + +**AC-CT-1:** Admin can create contribution type with name, amount, interval, description +**AC-CT-2:** Interval field is immutable after creation (validation error on change attempt) +**AC-CT-3:** Admin can update name, amount, description (but not interval) +**AC-CT-4:** Cannot delete contribution type if assigned to members +**AC-CT-5:** Cannot delete contribution type if periods exist referencing it +**AC-CT-6:** Interval must be one of: monthly, quarterly, half_yearly, yearly + +### ContributionPeriod Resource + +**AC-CP-1:** Period has period_start, status, amount, notes, member_id, contribution_type_id +**AC-CP-2:** Period_end is calculated, not stored +**AC-CP-3:** Status defaults to :unpaid +**AC-CP-4:** One period per member per period_start (uniqueness constraint) +**AC-CP-5:** Amount is set at generation time from contribution_type.amount +**AC-CP-6:** Periods cascade delete when member deleted +**AC-CP-7:** Admin/Treasurer can change status +**AC-CP-8:** Member can read own periods + +### Member Extensions + +**AC-M-1:** Member has contribution_type_id field (NOT NULL with default) +**AC-M-2:** Member has contribution_start_date field (nullable) +**AC-M-3:** New members get default contribution type from global setting +**AC-M-4:** contribution_start_date auto-set based on joined_at and global setting +**AC-M-5:** Admin can manually override contribution_start_date +**AC-M-6:** Cannot change to contribution type with different interval (MVP) + +### Period Generation + +**AC-PG-1:** Periods generated when member gets contribution type +**AC-PG-2:** Periods generated when member created (via change hook) +**AC-PG-3:** Scheduled job generates missing periods daily +**AC-PG-4:** Generation respects contribution_start_date +**AC-PG-5:** Generation stops at left_at if member exited +**AC-PG-6:** Generation is idempotent (skips existing periods) +**AC-PG-7:** Periods align to calendar boundaries (1st of month/quarter/half/year) +**AC-PG-8:** Amount comes from contribution_type at generation time + +### Calendar Logic + +**AC-CL-1:** Monthly periods: 1st to last day of month +**AC-CL-2:** Quarterly periods: 1st of Jan/Apr/Jul/Oct to last day of quarter +**AC-CL-3:** Half-yearly periods: 1st of Jan/Jul to last day of half +**AC-CL-4:** Yearly periods: Jan 1 to Dec 31 +**AC-CL-5:** Period_end calculated correctly for all interval types +**AC-CL-6:** Current period determined correctly based on today's date +**AC-CL-7:** Last completed period determined correctly + +### Contribution Type Change + +**AC-TC-1:** Can change to type with same interval +**AC-TC-2:** Cannot change to type with different interval (error message) +**AC-TC-3:** On allowed change: future unpaid periods regenerated +**AC-TC-4:** On allowed change: paid/suspended periods unchanged +**AC-TC-5:** On allowed change: amount updated to new type's amount +**AC-TC-6:** Change is atomic (transaction) + +### Settings + +**AC-S-1:** Global setting: include_joining_period (boolean, default true) +**AC-S-2:** Global setting: default_contribution_type_id (UUID, required) +**AC-S-3:** Admin can modify settings via UI +**AC-S-4:** Settings validated (e.g., default type must exist) +**AC-S-5:** Settings applied to new members immediately + +### UI - Member List + +**AC-UI-ML-1:** New column shows contribution status +**AC-UI-ML-2:** Default: Shows last completed period status +**AC-UI-ML-3:** Optional: Toggle to show current period status +**AC-UI-ML-4:** Color coding: green (paid), red (unpaid), gray (suspended) +**AC-UI-ML-5:** Filter: Unpaid in last period +**AC-UI-ML-6:** Filter: Unpaid in current period + +### UI - Member Detail + +**AC-UI-MD-1:** Contributions section shows all periods +**AC-UI-MD-2:** Table columns: Period, Interval, Amount, Status, Actions +**AC-UI-MD-3:** Checkbox per period for bulk marking (low prio) +**AC-UI-MD-4:** "Mark selected as paid" button +**AC-UI-MD-5:** Dropdown to change contribution type (same interval only) +**AC-UI-MD-6:** Warning if different interval selected +**AC-UI-MD-7:** Only show actions if user has permission + +### UI - Contribution Types Admin + +**AC-UI-CTA-1:** List all contribution types +**AC-UI-CTA-2:** Show: Name, Amount, Interval, Member count +**AC-UI-CTA-3:** Create new contribution type form +**AC-UI-CTA-4:** Edit form: Name, Amount, Description editable +**AC-UI-CTA-5:** Edit form: Interval grayed out (not editable) +**AC-UI-CTA-6:** Warning on amount change (explain impact) +**AC-UI-CTA-7:** Cannot delete if members assigned +**AC-UI-CTA-8:** Only admin can access + +### UI - Settings Admin + +**AC-UI-SA-1:** Contributions section in settings +**AC-UI-SA-2:** Dropdown to select default contribution type +**AC-UI-SA-3:** Checkbox: Include joining period +**AC-UI-SA-4:** Explanatory text with examples +**AC-UI-SA-5:** Save button with validation + +--- + +## Testing Strategy + +### Unit Testing + +**Period Generator Tests:** +- Correct period_start calculation for all interval types +- Correct period count from start to end date +- Respects contribution_start_date boundary +- Respects left_at boundary +- Skips existing periods (idempotent) +- Handles edge dates (year boundaries, leap years) + +**Calendar Periods Tests:** +- Period boundaries correct for all intervals +- Period_end calculation correct +- Current period detection +- Last completed period detection +- Next period calculation + +**Validation Tests:** +- Interval immutability enforced +- Same interval validation on type change +- Status transitions allowed +- Uniqueness constraints enforced + +### Integration Testing + +**Period Generation Flow:** +- Member creation triggers generation +- Type assignment triggers generation +- Type change regenerates future periods +- Scheduled job generates missing periods +- Left member stops generation + +**Status Management Flow:** +- Mark single period as paid +- Bulk mark multiple periods (low prio) +- Status transitions work +- Permissions enforced + +**Contribution Type Management:** +- Create type +- Update amount (regeneration triggered) +- Cannot update interval +- Cannot delete if in use + +### LiveView Testing + +**Member List:** +- Status column displays correctly +- Toggle between last/current works +- Filters work correctly +- Color coding applied + +**Member Detail:** +- Periods table displays all periods +- Checkboxes work +- Bulk marking works (low prio) +- Type change validation works +- Actions only shown with permission + +**Admin UI:** +- Type CRUD works +- Settings save correctly +- Validations display errors +- Only authorized users can access + +### Edge Case Testing + +**Interval Change Attempt:** +- Error message displayed +- No data modified +- User can cancel/choose different type + +**Exit with Unpaid:** +- Warning shown +- Option to suspend offered +- Exit completes correctly + +**Amount Change:** +- Warning displayed +- Only future unpaid regenerated +- Historical periods unchanged + +**Date Boundaries:** +- Today = period start handled +- Today = period end handled +- Leap year handled + +### Performance Testing + +**Period Generation:** +- Generate 10 years of monthly periods: < 100ms +- Generate for 1000 members: < 5 seconds +- Idempotent check efficient (no full scan) + +**Member List Query:** +- With status column: < 200ms for 1000 members +- Filters applied efficiently +- No N+1 queries + +--- + +## Security Considerations + +### Authorization + +**Permissions Required:** +- ContributionType management: Admin only +- ContributionPeriod status changes: Admin + Treasurer +- View all periods: Admin + Treasurer + Board +- View own periods: All authenticated users + +**Policy Enforcement:** +- All actions protected by Ash policies +- UI shows/hides based on permissions +- Backend validates permissions (never trust UI alone) + +### Data Integrity + +**Validation Layers:** +1. Database constraints (NOT NULL, UNIQUE, CHECK) +2. Ash validations (business rules) +3. UI validations (user experience) + +**Immutability Protection:** +- Interval change prevented at multiple layers +- Period amounts immutable (audit trail) +- Settings changes logged (future) + +### Audit Trail + +**Tracked Information:** +- Period status changes (who, when) - future enhancement +- Type amount changes (implicit via period amounts) +- Member type assignments (via timestamps) + +--- + +## Performance Considerations + +### Database Indexes + +**Required Indexes:** +- `contribution_periods(member_id)` - For member period lookups +- `contribution_periods(contribution_type_id)` - For type queries +- `contribution_periods(status)` - For unpaid filters +- `contribution_periods(period_start)` - For date range queries +- `contribution_periods(member_id, period_start)` - Composite unique index +- `members(contribution_type_id)` - For type membership count + +### Query Optimization + +**Preloading:** +- Load contribution_type with periods (avoid N+1) +- Load periods when displaying member detail +- Use Ash's load for efficient preloading + +**Calculated Fields:** +- period_end calculated on-demand (not stored) +- current_period_status calculated when needed +- Use Ash calculations for lazy evaluation + +**Pagination:** +- Period list paginated if > 50 periods +- Member list already paginated + +### Caching Strategy + +**No caching needed in MVP:** +- Contribution types rarely change +- Period queries are fast +- Settings read infrequently + +**Future caching if needed:** +- Cache settings in application memory +- Cache contribution types list +- Invalidate on change + +### Scheduled Job Performance + +**Period Generation Job:** +- Run daily or weekly (not hourly) +- Batch members (process 100 at a time) +- Skip members with no changes +- Log failures for retry + +--- + +## Future Enhancements + +### Phase 2: Interval Change Support + +**Architecture Changes:** +- Add logic to handle period overlaps +- Calculate prorata amounts if needed +- More complex validation +- Migration path for existing periods + +### Phase 3: Payment Details + +**Architecture Changes:** +- Add PaymentTransaction resource +- Link transactions to periods +- Support multiple payments per period +- Reconciliation logic + +### Phase 4: vereinfacht.digital Integration + +**Architecture Changes:** +- External API client module +- Webhook handling for transactions +- Automatic matching logic +- Manual review interface + +--- + +**End of Architecture Document** + diff --git a/docs/contributions-overview.md b/docs/contributions-overview.md new file mode 100644 index 0000000..e0c4bc8 --- /dev/null +++ b/docs/contributions-overview.md @@ -0,0 +1,527 @@ +# Membership Contributions - Overview + +**Project:** Mila - Membership Management System +**Feature:** Membership Contribution Management +**Version:** 1.0 +**Last Updated:** 2025-11-27 +**Status:** Concept - Ready for Review + +--- + +## Purpose + +This document provides a comprehensive overview of the Membership Contributions system. It covers business logic, data model, UI/UX design, and technical architecture in a concise, bullet-point format. + +**For detailed implementation:** See [contributions-implementation-plan.md](./contributions-implementation-plan.md) (created after concept iterations) + +--- + +## Table of Contents + +1. [Core Principle](#core-principle) +2. [Terminology](#terminology) +3. [Data Model](#data-model) +4. [Business Logic](#business-logic) +5. [UI/UX Design](#uiux-design) +6. [Edge Cases](#edge-cases) +7. [Technical Integration](#technical-integration) +8. [Implementation Scope](#implementation-scope) + +--- + +## Core Principle + +**Maximum Simplicity:** + +- Minimal complexity +- Clear data model without redundancies +- Intuitive operation +- Calendar period-based (Month/Quarter/Half-Year/Year) + +--- + +## Terminology + +### German ↔ English + +**Core Entities:** + +- Beitragsart ↔ Contribution Type / Membership Fee Type +- Beitragsintervall ↔ Contribution Period +- Mitgliedsbeitrag ↔ Membership Fee / Contribution + +**Status:** + +- bezahlt ↔ paid +- unbezahlt ↔ unpaid +- ausgesetzt ↔ suspended / waived + +**Intervals:** + +- monatlich ↔ monthly +- quartalsweise ↔ quarterly +- halbjährlich ↔ half-yearly / semi-annually +- jährlich ↔ yearly / annually + +**UI Elements:** + +- "Letztes Intervall" ↔ "Last Period" (e.g., 2023 when in 2024) +- "Aktuelles Intervall" ↔ "Current Period" (e.g., 2024) +- "Als bezahlt markieren" ↔ "Mark as paid" +- "Aussetzen" ↔ "Suspend" / "Waive" + +--- + +## Data Model + +### Contribution Type (ContributionType) + +``` +- id (UUID) +- name (String) - e.g., "Regular", "Reduced", "Student" +- amount (Decimal) - Contribution amount in Euro +- interval (Enum) - :monthly, :quarterly, :half_yearly, :yearly +- description (Text, optional) +- timestamps +``` + +**Important:** + +- `interval` is **IMMUTABLE** after creation! +- Admin can only change `name`, `amount`, `description` +- On change: Future unpaid periods regenerated with new amount + +### Contribution Period (ContributionPeriod) + +``` +- id (UUID) +- member_id (FK → members.id) +- contribution_type_id (FK → contribution_types.id) +- period_start (Date) - Calendar period start (01.01., 01.04., 01.07., 01.10., etc.) +- status (Enum) - :unpaid (default), :paid, :suspended +- amount (Decimal) - Amount at generation time (history when type changes) +- notes (Text, optional) - Admin notes +- timestamps +``` + +**Important:** + +- **NO** `period_end` - calculated from `period_start` + `interval` +- **NO** `interval_type` - read from `contribution_type.interval` +- Avoids redundancy and inconsistencies! + +**Calendar Period Logic:** + +- Monthly: 01.01. - 31.01., 01.02. - 28./29.02., etc. +- Quarterly: 01.01. - 31.03., 01.04. - 30.06., 01.07. - 30.09., 01.10. - 31.12. +- Half-yearly: 01.01. - 30.06., 01.07. - 31.12. +- Yearly: 01.01. - 31.12. + +### Member (Extensions) + +``` +- contribution_type_id (FK → contribution_types.id, NOT NULL, default from settings) +- contribution_start_date (Date, nullable) - When to start generating contributions +- left_at (Date, nullable) - Exit date (existing) +``` + +**Logic for contribution_start_date:** + +- Auto-set based on global setting `include_joining_period` +- If `include_joining_period = true`: First day of joining month/quarter/year +- If `include_joining_period = false`: First day of NEXT period after joining +- Can be manually overridden by admin + +**NO** `include_joining_period` field on Member - unnecessary due to `contribution_start_date`! + +### Global Settings + +``` +key: "contributions.include_joining_period" +value: Boolean (Default: true) + +key: "contributions.default_contribution_type_id" +value: UUID (Required) - Default contribution type for new members +``` + +**Meaning include_joining_period:** + +- `true`: Joining period is included (member pays from joining period) +- `false`: Only from next full period after joining + +**Meaning default_contribution_type_id:** + +- Every new member automatically gets this contribution type +- Must be configured in admin settings +- Prevents: Members without contribution type + +--- + +## Business Logic + +### Period Generation + +**Triggers:** + +- Member gets contribution type assigned (also during member creation) +- New period begins (Cron job daily/weekly) +- Admin requests manual regeneration + +**Algorithm:** + +1. Get `member.contribution_start_date` and `member.contribution_type` +2. Calculate first period based on `contribution_start_date` +3. Generate all periods from start to today (or `left_at` if present) +4. Skip existing periods +5. Set `amount` to current `contribution_type.amount` + +**Example (Yearly):** + +``` +Joining date: 15.03.2023 +include_joining_period: true +→ contribution_start_date: 01.01.2023 + +Generated periods: +- 01.01.2023 - 31.12.2023 (joining period) +- 01.01.2024 - 31.12.2024 +- 01.01.2025 - 31.12.2025 (current year) +``` + +**Example (Quarterly):** + +``` +Joining date: 15.03.2023 +include_joining_period: false +→ contribution_start_date: 01.04.2023 + +Generated periods: +- 01.04.2023 - 30.06.2023 (first full quarter) +- 01.07.2023 - 30.09.2023 +- 01.10.2023 - 31.12.2023 +- 01.01.2024 - 31.03.2024 +- ... +``` + +### Status Transitions + +``` +unpaid → paid +unpaid → suspended +paid → unpaid +suspended → paid +suspended → unpaid +``` + +**Permissions:** + +- Admin + Treasurer (Kassenwart) can change status +- Uses existing permission system + +### Contribution Type Change + +**MVP - Same Interval Only:** + +- Member can only choose contribution type with **same interval** +- Example: From "Regular (yearly)" to "Reduced (yearly)" ✓ +- Example: From "Regular (yearly)" to "Reduced (monthly)" ✗ + +**Logic on Change:** + +1. Check: New contribution type has same interval +2. If yes: Set `member.contribution_type_id` +3. Future **unpaid** periods: Delete and regenerate with new amount +4. Paid/suspended periods: Remain unchanged (historical amount) + +**Future - Different Intervals:** + +- Enable interval switching (e.g., yearly → monthly) +- More complex logic for period overlaps +- Needs additional validation + +### Member Exit + +**Logic:** + +- Periods only generated until `member.left_at` +- Existing periods remain visible +- Unpaid exit period can be marked as "suspended" + +**Example:** + +``` +Exit: 15.08.2024 +Yearly period: 01.01.2024 - 31.12.2024 + +→ Period 2024 is shown (Status: unpaid) +→ Admin can set to "suspended" +→ No periods for 2025+ generated +``` + +--- + +## UI/UX Design + +### Member List View + +**New Column: "Contribution Status"** + +**Default Display (Last Period):** + +- Shows status of **last completed** period +- Example in 2024: Shows contribution for 2023 +- Color coding: + - Green: paid ✓ + - Red: unpaid ✗ + - Gray: suspended ⊘ + +**Optional: Show Current Period** + +- Toggle: "Show current period" (2024) +- Admin decides what to display + +**Filters:** + +- "Unpaid contributions in last period" +- "Unpaid contributions in current period" + +### Member Detail View + +**Section: "Contributions"** + +**Contribution Type Assignment:** + +``` +┌─────────────────────────────────────┐ +│ Contribution Type: [Dropdown] │ +│ ⚠ Only types with same interval │ +│ can be selected │ +└─────────────────────────────────────┘ +``` + +**Period Table:** + +``` +┌───────────────┬──────────┬────────┬──────────┬─────────┐ +│ Period │ Interval │ Amount │ Status │ Action │ +├───────────────┼──────────┼────────┼──────────┼─────────┤ +│ 01.01.2023- │ Yearly │ 50 € │ ☑ Paid │ │ +│ 31.12.2023 │ │ │ │ │ +├───────────────┼──────────┼────────┼──────────┼─────────┤ +│ 01.01.2024- │ Yearly │ 60 € │ ☐ Open │ [Mark │ +│ 31.12.2024 │ │ │ │ as paid]│ +├───────────────┼──────────┼────────┼──────────┼─────────┤ +│ 01.01.2025- │ Yearly │ 60 € │ ☐ Open │ [Mark │ +│ 31.12.2025 │ │ │ │ as paid]│ +└───────────────┴──────────┴────────┴──────────┴─────────┘ + +Legend: ☑ = paid | ☐ = unpaid | ⊘ = suspended +``` + +**Quick Marking:** + +- Checkbox in each row for fast marking +- Button: "Mark selected as paid/unpaid/suspended" +- Bulk action for multiple periods + +### Admin: Contribution Types Management + +**List:** + +``` +┌────────────┬──────────┬──────────┬────────────┬─────────┐ +│ Name │ Amount │ Interval │ Members │ Actions │ +├────────────┼──────────┼──────────┼────────────┼─────────┤ +│ Regular │ 60 € │ Yearly │ 45 │ [Edit] │ +│ Reduced │ 30 € │ Yearly │ 12 │ [Edit] │ +│ Student │ 20 € │ Monthly │ 8 │ [Edit] │ +└────────────┴──────────┴──────────┴────────────┴─────────┘ +``` + +**Edit:** + +- Name: ✓ editable +- Amount: ✓ editable +- Description: ✓ editable +- Interval: ✗ **NOT** editable (grayed out) + +**Warning on Amount Change:** + +``` +⚠ Change amount to 65 €? + +Impact: +- 45 members affected +- Future unpaid periods will be generated with 65 € +- Already paid periods remain with old amount + +[Cancel] [Confirm] +``` + +### Admin: Settings + +**Contribution Configuration:** + +``` +Default Contribution Type: [Dropdown: Contribution Types] + +Selected: "Regular (60 €, Yearly)" + +This contribution type is automatically assigned to all new members. +Can be changed individually per member. + +--- + +☐ Include joining period + +When active: +Members pay from the period of their joining. + +Example (Yearly): +Joining: 15.03.2023 +→ Pays from 2023 + +When inactive: +Members pay from the next full period. + +Example (Yearly): +Joining: 15.03.2023 +→ Pays from 2024 +``` + +--- + +## Edge Cases + +### 1. Contribution Type Change with Different Interval + +**MVP:** Blocked (only same interval allowed) + +**UI:** + +``` +Error: Interval change not possible + +Current contribution type: "Regular (Yearly)" +Selected contribution type: "Student (Monthly)" + +Changing the interval is currently not possible. +Please select a contribution type with interval "Yearly". + +[OK] +``` + +**Future:** + +- Allow interval switching +- Calculate overlaps +- Generate new periods without duplicates + +### 2. Exit with Unpaid Contributions + +**Scenario:** + +``` +Member exits: 15.08.2024 +Yearly period 2024: unpaid +``` + +**UI Notice on Exit: (Low Prio)** + +``` +⚠ Unpaid contributions present + +This member has 1 unpaid period(s): +- 2024: 60 € (unpaid) + +Do you want to continue? + +[ ] Mark contribution as "suspended" +[Cancel] [Confirm Exit] +``` + +### 3. Multiple Unpaid Periods + +**Scenario:** Member hasn't paid for 2 years + +**Display:** + +``` +┌───────────────┬──────────┬────────┬──────────┬─────────┐ +│ 2023 │ Yearly │ 50 € │ ☐ Open │ [✓] │ +│ 2024 │ Yearly │ 60 € │ ☐ Open │ [✓] │ +│ 2025 │ Yearly │ 60 € │ ☐ Open │ [ ] │ +└───────────────┴──────────┴────────┴──────────┴─────────┘ + +[Mark selected as paid/unpaid/suspended] (2 selected) +``` + +### 4. Amount Changes + +**Scenario:** + +``` +2023: Regular = 50 € +2024: Regular = 60 € (increase) +``` + +**Result:** + +- Period 2023: Saved with 50 € (history) +- Period 2024: Generated with 60 € (current) +- Both periods show correct historical amount + +### 5. Date Boundaries + +**Problem:** What if today = 01.01.2025? + +**Solution:** + +- Current period (2025) is generated +- Status: unpaid (open) +- Shown in overview + +--- + +## Implementation Scope + +### MVP (Phase 1) + +**Included:** + +- ✓ Contribution types (CRUD) +- ✓ Automatic period generation +- ✓ Status management (paid/unpaid/suspended) +- ✓ Member overview with contribution status +- ✓ Period view per member +- ✓ Quick checkbox marking +- ✓ Bulk actions +- ✓ Amount history +- ✓ Same-interval type change +- ✓ Default contribution type +- ✓ Joining period configuration + +**NOT Included:** + +- ✗ Interval change (only same interval) +- ✗ Payment details (date, method) +- ✗ Automatic integration (vereinfacht.digital) +- ✗ Prorata calculation +- ✗ Reports/statistics +- ✗ Reminders/dunning (manual via filters) + +### Future Enhancements + +**Phase 2:** + +- Payment details (date, amount, method) +- Interval change for future unpaid periods +- Manual vereinfacht.digital links per member +- Extended filter options + +**Phase 3:** + +- Automated vereinfacht.digital integration +- Automatic payment matching +- SEPA integration +- Advanced reports diff --git a/docs/feature-roadmap.md b/docs/feature-roadmap.md index 609523c..2f86f5e 100644 --- a/docs/feature-roadmap.md +++ b/docs/feature-roadmap.md @@ -187,10 +187,16 @@ **Current State:** - ✅ Basic "paid" boolean field on members +- ✅ **UI Mock-ups for Contribution Types & Settings** (2025-12-02) - ⚠️ No payment tracking **Open Issues:** - [#156](https://git.local-it.org/local-it/mitgliederverwaltung/issues/156) - Set up & document testing environment for vereinfacht.digital (L, Low priority) +- [#226](https://git.local-it.org/local-it/mitgliederverwaltung/issues/226) - Payment/Contribution Mockup Pages (Preview) + +**Mock-Up Pages (Non-Functional Preview):** +- `/contribution_types` - Contribution Types Management +- `/contribution_settings` - Global Contribution Settings **Missing Features:** - ❌ Membership fee configuration diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index 54a5a64..08133b5 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,61 +368,63 @@ 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 - "" + if rendered == "" do + "" + else + rendered + end else - rendered - end - else - "" - end} - -
- <%= for action <- @action do %> - {render_slot(action, @row_item.(row))} - <% end %> -
-
+ "" + 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 7ff7f25..1c1138e 100644 --- a/lib/mv_web/components/layouts/navbar.ex +++ b/lib/mv_web/components/layouts/navbar.ex @@ -25,6 +25,17 @@ defmodule MvWeb.Layouts.Navbar do
  • <.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 new file mode 100644 index 0000000..eaa9271 --- /dev/null +++ b/lib/mv_web/helpers/date_formatter.ex @@ -0,0 +1,27 @@ +defmodule MvWeb.Helpers.DateFormatter do + @moduledoc """ + Centralized date formatting helper for the application. + Formats dates in European format (dd.mm.yyyy). + """ + + use Gettext, backend: MvWeb.Gettext + + @doc """ + Formats a Date struct to European format (dd.mm.yyyy). + + ## Examples + + iex> MvWeb.Helpers.DateFormatter.format_date(~D[2024-03-15]) + "15.03.2024" + + iex> MvWeb.Helpers.DateFormatter.format_date(nil) + "" + """ + def format_date(%Date{} = date) do + Calendar.strftime(date, "%d.%m.%Y") + end + + def format_date(nil), do: "" + + def format_date(_), do: "Invalid date" +end diff --git a/lib/mv_web/live/components/sort_header_component.ex b/lib/mv_web/live/components/sort_header_component.ex index b847308..3817d90 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 new file mode 100644 index 0000000..713bc8c --- /dev/null +++ b/lib/mv_web/live/contribution_settings_live.ex @@ -0,0 +1,277 @@ +defmodule MvWeb.ContributionSettingsLive do + @moduledoc """ + Mock-up LiveView for Contribution Settings (Admin). + + This is a preview-only page that displays the planned UI for managing + global contribution settings. It shows static mock data and is not functional. + + ## Planned Features (Future Implementation) + - Set default contribution type for new members + - Configure whether joining period is included in contributions + - Explanatory text with examples + + ## Settings + - `default_contribution_type_id` - UUID of the default contribution type + - `include_joining_period` - Boolean whether to include joining period + + ## Note + This page is intentionally non-functional and serves as a UI mockup + for the upcoming Membership Contributions feature. + """ + use MvWeb, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:page_title, gettext("Contribution Settings")) + |> assign(:contribution_types, mock_contribution_types()) + |> assign(:selected_type_id, "1") + |> assign(:include_joining_period, true)} + end + + @impl true + def render(assigns) do + ~H""" + + <.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 new file mode 100644 index 0000000..9a7b602 --- /dev/null +++ b/lib/mv_web/live/contribution_type_live/index.ex @@ -0,0 +1,205 @@ +defmodule MvWeb.ContributionTypeLive.Index do + @moduledoc """ + Mock-up LiveView for Contribution Types Management (Admin). + + This is a preview-only page that displays the planned UI for managing + contribution types. It shows static mock data and is not functional. + + ## Planned Features (Future Implementation) + - List all contribution types + - Display: Name, Amount, Interval, Member count + - Create new contribution types + - Edit existing contribution types (name, amount, description - NOT interval) + - Delete contribution types (if no members assigned) + + ## Note + This page is intentionally non-functional and serves as a UI mockup + for the upcoming Membership Contributions feature. + """ + use MvWeb, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, + socket + |> assign(:page_title, gettext("Contribution Types")) + |> assign(:contribution_types, mock_contribution_types())} + end + + @impl true + def render(assigns) do + ~H""" + + <.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 67ce522..792466c 100644 --- a/lib/mv_web/live/member_live/index.ex +++ b/lib/mv_web/live/member_live/index.ex @@ -32,6 +32,7 @@ defmodule MvWeb.MemberLive.Index do alias Mv.Membership alias MvWeb.MemberLive.Index.Formatter + alias MvWeb.Helpers.DateFormatter # Prefix used in sort field names for custom fields (e.g., "custom_field_") @custom_field_prefix "custom_field_" @@ -937,4 +938,7 @@ defmodule MvWeb.MemberLive.Index do Map.get(visibility_config, Atom.to_string(field), true) end) end + + # Public helper function to format dates for use in templates + def format_date(date), do: DateFormatter.format_date(date) end diff --git a/lib/mv_web/live/member_live/index.html.heex b/lib/mv_web/live/member_live/index.html.heex index 9f8851b..959a3bc 100644 --- a/lib/mv_web/live/member_live/index.html.heex +++ b/lib/mv_web/live/member_live/index.html.heex @@ -224,7 +224,7 @@ """ } > - {member.join_date} + {MvWeb.MemberLive.Index.format_date(member.join_date)} <:col :let={member} label={gettext("Paid")}> Date.to_string(date) + {:ok, date} -> DateFormatter.format_date(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 de46a3a..7601f46 100644 --- a/lib/mv_web/live/member_live/show.ex +++ b/lib/mv_web/live/member_live/show.ex @@ -23,6 +23,7 @@ defmodule MvWeb.MemberLive.Show do """ use MvWeb, :live_view import Ash.Query + alias MvWeb.Helpers.DateFormatter @impl true def render(assigns) do @@ -52,8 +53,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")}>{@member.join_date} - <:item title={gettext("Exit Date")}>{@member.exit_date} + <: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("Notes")}>{@member.notes} <:item title={gettext("City")}>{@member.city} <:item title={gettext("Street")}>{@member.street} @@ -81,10 +82,7 @@ defmodule MvWeb.MemberLive.Show do # name cfv.custom_field && cfv.custom_field.name, # value - case cfv.value do - %{value: v} -> v - v -> v - end + format_custom_field_value(cfv) } end) } /> @@ -114,4 +112,17 @@ 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 9619a15..0639e75 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 for={@form} id="user-form" phx-change="validate" phx-submit="save"> + <.form class="max-w-xl" for={@form} id="user-form" phx-change="validate" phx-submit="save"> <.input field={@form[:email]} label={gettext("Email")} required type="email" /> @@ -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,10 +245,12 @@ 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 3582046..9a98159 100644 --- a/lib/mv_web/live/user_live/index.html.heex +++ b/lib/mv_web/live/user_live/index.html.heex @@ -49,7 +49,6 @@ > {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 664f99f..777def1 100644 --- a/lib/mv_web/live/user_live/show.ex +++ b/lib/mv_web/live/user_live/show.ex @@ -46,9 +46,7 @@ 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")} @@ -56,13 +54,13 @@ defmodule MvWeb.UserLive.Show do <%= if @user.member do %> <.link navigate={~p"/members/#{@user.member}"} - class="text-blue-600 hover:text-blue-800 underline" + class="text-blue-600 underline hover:text-blue-800" > - <.icon name="hero-users" class="h-4 w-4 inline mr-1" /> + <.icon name="hero-users" class="inline w-4 h-4 mr-1" /> {@user.member.first_name} {@user.member.last_name} <% 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 09a2792..c574e17 100644 --- a/lib/mv_web/router.ex +++ b/lib/mv_web/router.ex @@ -75,6 +75,11 @@ defmodule MvWeb.Router do live "/settings", GlobalSettingsLive + # Contribution Management (Mock-ups) + live "/contribution_types", ContributionTypeLive.Index, :index + live "/contribution_settings", ContributionSettingsLive + live "/contributions/member/:id", ContributionPeriodLive.Show, :show + post "/set_locale", LocaleController, :set_locale end diff --git a/mix.exs b/mix.exs index c6e4fb5..3cbc8a3 100644 --- a/mix.exs +++ b/mix.exs @@ -37,7 +37,7 @@ defmodule Mv.MixProject do [ {:tidewave, "~> 0.5", only: [:dev]}, {:sourceror, "~> 1.8", only: [:dev, :test]}, - {:live_debugger, "~> 0.4", only: [:dev]}, + {:live_debugger, "~> 0.5", only: [:dev]}, {:ash_admin, "~> 0.13"}, {:ash_postgres, "~> 2.0"}, {:ash_phoenix, "~> 2.0"}, @@ -45,7 +45,7 @@ defmodule Mv.MixProject do {:bcrypt_elixir, "~> 3.0"}, {:ash_authentication, "~> 4.9"}, {:ash_authentication_phoenix, "~> 2.10"}, - {:igniter, "~> 0.6", only: [:dev, :test]}, + {:igniter, "~> 0.7", only: [:dev, :test]}, {:phoenix, "~> 1.8.0-rc.4", override: true}, {:phoenix_ecto, "~> 4.5"}, {:ecto_sql, "~> 3.10"}, diff --git a/mix.lock b/mix.lock index 77dcc09..fb56d5d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,32 +1,32 @@ %{ - "ash": {:hex, :ash, "3.7.1", "abb55dee19e0959e529e52fe0622468825ae05400f535484919713e492d9a9e7", [:mix], [{:crux, "~> 0.1.0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4474ce9befe9862d1ed73cadf8a755e836c45a14a7b3b952d02e1a12f2b2e529"}, - "ash_admin": {:hex, :ash_admin, "0.13.19", "43227905381ea0b835039fb3f3d255a3664925619937869e605402bc2f95c5e5", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "41e6262c437164df6f052e43cc93be225a7e148b49a813fc451e70172338ee38"}, - "ash_authentication": {:hex, :ash_authentication, "4.11.0", "4165ede37e179cb0a24b7bfc38d620fa93c05fb6272fbd353cafe27652b1e68b", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, ">= 3.4.29 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "~> 0.2.13", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "8201d0169944c1df3db9b560494e50e1c3bc99c3b1a8a2ef1e61b0f77bc820df"}, - "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.12.0", "75d7d77e3b626f3d8ea6ee44291d885950172ab399d997b2934f93d2e0a55a61", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "a423e22b40fdf3b1a7f2178e44ca68f48fdb5ba0d87e8d42a43de1a3b63ca704"}, - "ash_phoenix": {:hex, :ash_phoenix, "2.3.17", "a074ae6d9d7135d99c4edc91ddebe4c035ca380b044592bf9c3d58471669cf52", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "94e4a6cc6ced31cddba930c45c1c3477aa59b956e7fc3cdc63095cf0e506bdf5"}, - "ash_postgres": {:hex, :ash_postgres, "2.6.23", "5976a7e5e204b7bc627b1d17026bec9da4d880f2e09cd94bf4e8cee41fef32ce", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.3.7 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "61de4aedfe30f1ae14d8185cfc37a5b1940b45b60f2dfbdf9eb056f97dca41c5"}, - "ash_sql": {:hex, :ash_sql, "0.3.7", "80affa5446075d71deb157c67290685a84b392d723be766bfb684f58fe0143de", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "ce4d974b8e784171c5a2a62593b3672b42dfd4888fa2239f01a6b32bad769038"}, + "ash": {:hex, :ash, "3.10.1", "e0a9cd71d439563734bbaf1580bdc201866e8597a8e1f0711b5140b9694a020f", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ac55578b6208f420ae02fe259b609dd274a0b5193a56a22f73c5dbb0db40bd8b"}, + "ash_admin": {:hex, :ash_admin, "0.13.23", "09f25429727b7c3313006a5c28a5e95d3f80f10989461f69ae3c46f01233aa1d", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1-rc", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "4d06e002313591e354ab25b4c82090261783fd8b50c773baea9f8a8ad370b834"}, + "ash_authentication": {:hex, :ash_authentication, "4.13.3", "4d7a2e96b5a8fe68797ba0124cf40e6897c82b9fb69182fc5fdaac529b72d436", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "03d95b68766b28cda241e68217f6d1d839be350f7e8f20923162b163fb521b91"}, + "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.12.2", "a4646498a7e21fbdbe372f0d8afab08b5d7125b629f91bfcf8f4d1961bc9d57b", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "1dd6fa3a8f7d2563a53cf22aeda31770c855e927421af4d8bfaf480332acf721"}, + "ash_phoenix": {:hex, :ash_phoenix, "2.3.18", "fad1b8af1405758888086de568f08650c2911ee97074cfe2e325b14854bc43dd", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "7ec28f9216221e83b90d9c3605e9d1cdd228984e09a1a86c9b9d393cebf25222"}, + "ash_postgres": {:hex, :ash_postgres, "2.6.26", "f995bac8762ae039d4fb94cf2b628430aa69b0b30bf4366b96b3543dbd679ae7", [:mix], [{:ash, "~> 3.9", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.3.12 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "7050b3169d5a31d73f7e69a6564d1102cb2bc185e67ea428e78fda3da46a69fc"}, + "ash_sql": {:hex, :ash_sql, "0.3.15", "8b8daae1870ab37b4fb2f980e323194caf23cdb4218fef126c49cc11a01fa243", [:mix], [{:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "97432507b6f406eb2461e5d0fbf2e5104a8c61a2570322d11de2f124d822d8ff"}, "assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"}, "bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, + "castore": {:hex, :castore, "1.0.16", "8a4f9a7c8b81cda88231a08fe69e3254f16833053b23fa63274b05cbc61d2a1e", [:mix], [], "hexpm", "33689203a0eaaf02fcd0e86eadfbcf1bd636100455350592e7e2628564022aaf"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "circular_buffer": {:hex, :circular_buffer, "1.0.0", "25c004da0cba7bd8bc1bdabded4f9a902d095e20600fd15faf1f2ffbaea18a07", [:mix], [], "hexpm", "c829ec31c13c7bafd1f546677263dff5bfb006e929f25635878ac3cfba8749e5"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, - "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, - "crux": {:hex, :crux, "0.1.1", "94f2f97d2a6079ae3c57f356412bc3b307f9579a80e43f526447b1d508dd4a72", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "e59d498f038193cbe31e448f9199f5b4c53a4c67cece9922bb839595189dd2b6"}, + "credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"}, + "crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"}, "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"}, - "ecto": {:hex, :ecto, "3.13.3", "6a983f0917f8bdc7a89e96f2bf013f220503a0da5d8623224ba987515b3f0d80", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1927db768f53a88843ff25b6ba7946599a8ca8a055f69ad8058a1432a399af94"}, + "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_phone_number": {:hex, :ex_phone_number, "0.4.8", "c1c5e6f0673822a2a7b439b43af7d3eb1a5c19ae582b772b8b8d12625dd51ec1", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "43e2357c6b8cfe556bcd417f4ce9aaef267a786e31a2938902daaa0d36f69757"}, - "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, + "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"}, @@ -35,14 +35,14 @@ "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "igniter": {:hex, :igniter, "0.6.30", "83a466369ebb8fe009e0823c7bf04314dc545122c2d48f896172fc79df33e99d", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "76a14d5b7f850bb03b5243088c3649d54a2e52e34a2aa1104dee23cf50a8bae0"}, + "igniter": {:hex, :igniter, "0.7.0", "6848714fa5afa14258c82924a57af9364745316241a409435cf39cbe11e3ae80", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "1e7254780dbf4b44c9eccd6d86d47aa961efc298d7f520c24acb0258c8e90ba9"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, - "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, + "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"}, "lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, - "live_debugger": {:hex, :live_debugger, "0.4.2", "775c3a570ef3c44d27d261b3c1aae23ef35cac949a57f67b3e7b1aa1fb2707bc", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.8 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "5b24e37985f0424056a322a18dab4a5fb0f4e8ee4e55975985364e0b45d683b9"}, + "live_debugger": {:hex, :live_debugger, "0.5.0", "95e0f7727d61010f7e9053923fb2a9416904a7533c2dfb36120e7684cba4c0af", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.8 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "73ebe95118d22aa402675f677abd731cb16b136d1b6ae5f4010441fb50753b14"}, "luhn": {:hex, :luhn, "0.3.3", "5aa0c6a32c2db4b9db9f9b883ba8301c1ae169d57199b9e6cb1ba2707bc51d96", [:mix], [], "hexpm", "3e823a913a25aab51352c727f135278d22954874d5f0835be81ed4fec3daf78d"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, @@ -50,26 +50,26 @@ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"}, - "phoenix": {:hex, :phoenix, "1.8.1", "865473a60a979551a4879db79fbfb4503e41cd809e77c85af79716578b6a456d", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "84d77d2b2e77c3c7e7527099bd01ef5c8560cd149c036d6b3a40745f11cd2fb2"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, + "phoenix": {:hex, :phoenix, "1.8.2", "75aba5b90081d88a54f2fc6a26453d4e76762ab095ff89be5a3e7cb28bff9300", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "19ea65b4064f17b1ab0515595e4d0ea65742ab068259608d5d7b139a73f47611"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.14", "cae84abc4cd00dde4bb200b8516db556704c585c267aff9cd4955ff83cceb86c", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b827980e2bc00fddd8674e3b567519a4e855b5de04bf8607140414f1101e2627"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.18", "b5410017b3d4edf261d9c98ebc334e0637d7189457c730720cfc13e206443d43", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f189b759595feff0420e9a1d544396397f9cf9e2d5a8cb98ba5b6cab01927da0"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, "reactor": {:hex, :reactor, "0.17.0", "eb8bdb530dbae824e2d36a8538f8ec4f3aa7c2d1b61b04959fa787c634f88b49", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "3c3bf71693adbad9117b11ec83cfed7d5851b916ade508ed9718de7ae165bf25"}, - "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, + "req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"}, "rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"}, "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, - "spark": {:hex, :spark, "2.3.5", "f30d30ecc3b4ab9b932d9aada66af7677fc1f297a2c349b0bcec3eafb9f996e8", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "0e9d339704d5d148f77f2b2fef3bcfc873a9e9bb4224fcf289c545d65827202f"}, + "spark": {:hex, :spark, "2.3.14", "a08420d08e6e0e49d740aed3e160f1cb894ba8f6b3f5e6c63253e9df1995265c", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "af50c4ea5dd67eba822247f1c98e1d4e598cb7f6c28ccf5d002f0e0718096f4f"}, "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"}, "splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"}, "stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"}, @@ -80,11 +80,11 @@ "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, - "thousand_island": {:hex, :thousand_island, "1.4.1", "8df065e627407e281f7935da5ad0f3842d10eb721afa92e760b720d71e2e37aa", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "204a8640e5d2818589b87286ae66160978628d7edf6095181cbe0440765fb6c1"}, - "tidewave": {:hex, :tidewave, "0.5.0", "8f278d7eb2d0af36ae6d4f73a5872bd066815bd57b57401125187ba901f095a4", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "9a1eb5d2f12ff4912328dfbfe652c27fded462c6ed6fd11814ee28d3e9d016b4"}, + "thousand_island": {:hex, :thousand_island, "1.4.2", "735fa783005d1703359bbd2d3a5a3a398075ba4456e5afe3c5b7cf4666303d36", [], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1c7637f16558fc1c35746d5ee0e83b18b8e59e18d28affd1f2fa1645f8bc7473"}, + "tidewave": {:hex, :tidewave, "0.5.2", "f549acffe9daeed8b6b547c232c60de987770da7f827f9b3300140dfc465b102", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "34ab3ffee7e402f05cd1eae68d0e77ed0e0d1925677971ef83634247553e8afd"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.12.0", "30343ff5018637a64b1b7de1ed2a3ca03bc641410c1f311a4dbdc1ffbbf449c7", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "ca6bacae7bac917a7155dca0ab6149088aa7bc800c94d0fe18c5238f53b313c6"}, "ymlr": {:hex, :ymlr, "5.1.4", "b924d61e1fc1ec371cde6ab3ccd9311110b1e052fc5c2460fb322e8380e7712a", [:mix], [], "hexpm", "75f16cf0709fbd911b30311a0359a7aa4b5476346c01882addefd5f2b1cfaa51"}, diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 57df5ab..be7ffef 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -11,6 +11,7 @@ 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" @@ -34,18 +35,20 @@ msgstr "Verbindung wird wiederhergestellt" msgid "City" msgstr "Stadt" +#: lib/mv_web/live/contribution_type_live/index.ex:78 #: lib/mv_web/live/member_live/index.html.heex:250 #: lib/mv_web/live/user_live/index.html.heex:74 #, 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/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 #, elixir-autogen, elixir-format msgid "Edit" -msgstr "Bearbeite" +msgstr "Bearbeiten" #: lib/mv_web/live/member_live/show.ex:41 #: lib/mv_web/live/member_live/show.ex:116 @@ -53,6 +56,7 @@ msgstr "Bearbeite" 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/show.ex:50 @@ -121,6 +125,7 @@ msgstr "Austrittsdatum" msgid "House Number" msgstr "Hausnummer" +#: lib/mv_web/live/contribution_period_live/show.ex:140 #: lib/mv_web/live/member_live/form.ex:52 #: lib/mv_web/live/member_live/show.ex:57 #, elixir-autogen, elixir-format @@ -129,6 +134,8 @@ msgstr "Notizen" #: lib/mv_web/live/components/payment_filter_component.ex:94 #: lib/mv_web/live/components/payment_filter_component.ex:144 +#: lib/mv_web/live/contribution_period_live/show.ex:186 +#: lib/mv_web/live/contribution_period_live/show.ex: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/show.ex:51 @@ -288,7 +295,7 @@ msgstr "ID" msgid "Immutable" msgstr "Unveränderlich" -#: lib/mv_web/components/layouts/navbar.ex:102 +#: lib/mv_web/components/layouts/navbar.ex:113 #, elixir-autogen, elixir-format msgid "Logout" msgstr "Abmelden" @@ -305,12 +312,14 @@ 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" @@ -331,6 +340,7 @@ 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 @@ -349,7 +359,7 @@ msgstr "OIDC ID" msgid "Password Authentication" msgstr "Passwort-Authentifizierung" -#: lib/mv_web/components/layouts/navbar.ex:95 +#: lib/mv_web/components/layouts/navbar.ex:106 #, elixir-autogen, elixir-format msgid "Profil" msgstr "Profil" @@ -369,7 +379,7 @@ msgstr "Alle Mitglieder auswählen" msgid "Select member" msgstr "Mitglied auswählen" -#: lib/mv_web/components/layouts/navbar.ex:99 +#: lib/mv_web/components/layouts/navbar.ex:110 #, elixir-autogen, elixir-format msgid "Settings" msgstr "Einstellungen" @@ -537,14 +547,14 @@ msgstr "Zurück zur Mitgliederliste" msgid "Back to users list" msgstr "Zurück zur Benutzer*innen-Liste" -#: lib/mv_web/components/layouts/navbar.ex:33 -#: lib/mv_web/components/layouts/navbar.ex:39 +#: lib/mv_web/components/layouts/navbar.ex:44 +#: lib/mv_web/components/layouts/navbar.ex:50 #, elixir-autogen, elixir-format msgid "Select language" msgstr "Sprache auswählen" -#: lib/mv_web/components/layouts/navbar.ex:46 -#: lib/mv_web/components/layouts/navbar.ex:66 +#: lib/mv_web/components/layouts/navbar.ex:57 +#: lib/mv_web/components/layouts/navbar.ex:77 #, elixir-autogen, elixir-format msgid "Toggle dark mode" msgstr "Dunklen Modus umschalten" @@ -716,6 +726,7 @@ msgstr "Vereinsdaten" msgid "Manage global settings for the association." msgstr "Passe übergreifende Einstellungen für den Verein an." +#: lib/mv_web/live/contribution_settings_live.ex:102 #: lib/mv_web/live/global_settings_live.ex:56 #, elixir-autogen, elixir-format, fuzzy msgid "Save Settings" @@ -853,6 +864,432 @@ 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 1e0e954..8594746 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -12,6 +12,7 @@ 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 "" @@ -35,12 +36,14 @@ msgstr "" msgid "City" msgstr "" +#: lib/mv_web/live/contribution_type_live/index.ex:78 #: lib/mv_web/live/member_live/index.html.heex:250 #: lib/mv_web/live/user_live/index.html.heex:74 #, 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/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 @@ -54,6 +57,7 @@ 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/show.ex:50 @@ -122,6 +126,7 @@ msgstr "" 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 @@ -130,6 +135,8 @@ msgstr "" #: lib/mv_web/live/components/payment_filter_component.ex:94 #: lib/mv_web/live/components/payment_filter_component.ex:144 +#: lib/mv_web/live/contribution_period_live/show.ex:186 +#: lib/mv_web/live/contribution_period_live/show.ex: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/show.ex:51 @@ -289,7 +296,7 @@ msgstr "" msgid "Immutable" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:102 +#: lib/mv_web/components/layouts/navbar.ex:113 #, elixir-autogen, elixir-format msgid "Logout" msgstr "" @@ -306,12 +313,14 @@ 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" @@ -332,6 +341,7 @@ 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 @@ -350,7 +360,7 @@ msgstr "" msgid "Password Authentication" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:95 +#: lib/mv_web/components/layouts/navbar.ex:106 #, elixir-autogen, elixir-format msgid "Profil" msgstr "" @@ -370,7 +380,7 @@ msgstr "" msgid "Select member" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:99 +#: lib/mv_web/components/layouts/navbar.ex:110 #, elixir-autogen, elixir-format msgid "Settings" msgstr "" @@ -538,14 +548,14 @@ msgstr "" msgid "Back to users list" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:33 -#: lib/mv_web/components/layouts/navbar.ex:39 +#: lib/mv_web/components/layouts/navbar.ex:44 +#: lib/mv_web/components/layouts/navbar.ex:50 #, elixir-autogen, elixir-format msgid "Select language" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:46 -#: lib/mv_web/components/layouts/navbar.ex:66 +#: lib/mv_web/components/layouts/navbar.ex:57 +#: lib/mv_web/components/layouts/navbar.ex:77 #, elixir-autogen, elixir-format msgid "Toggle dark mode" msgstr "" @@ -717,6 +727,7 @@ msgstr "" msgid "Manage global settings for the association." msgstr "" +#: lib/mv_web/live/contribution_settings_live.ex:102 #: lib/mv_web/live/global_settings_live.ex:56 #, elixir-autogen, elixir-format msgid "Save Settings" @@ -853,3 +864,429 @@ 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 319bcc3..cf081bf 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -12,6 +12,7 @@ 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 "" @@ -35,12 +36,14 @@ msgstr "" msgid "City" msgstr "" +#: lib/mv_web/live/contribution_type_live/index.ex:78 #: lib/mv_web/live/member_live/index.html.heex:250 #: lib/mv_web/live/user_live/index.html.heex:74 #, 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/user_live/form.ex:265 #: lib/mv_web/live/user_live/index.html.heex:66 @@ -54,6 +57,7 @@ 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/show.ex:50 @@ -122,6 +126,7 @@ msgstr "" 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 @@ -130,6 +135,8 @@ msgstr "" #: lib/mv_web/live/components/payment_filter_component.ex:94 #: lib/mv_web/live/components/payment_filter_component.ex:144 +#: lib/mv_web/live/contribution_period_live/show.ex:186 +#: lib/mv_web/live/contribution_period_live/show.ex: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/show.ex:51 @@ -289,7 +296,7 @@ msgstr "" msgid "Immutable" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:102 +#: lib/mv_web/components/layouts/navbar.ex:113 #, elixir-autogen, elixir-format msgid "Logout" msgstr "" @@ -306,12 +313,14 @@ 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" @@ -332,6 +341,7 @@ 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 @@ -350,7 +360,7 @@ msgstr "" msgid "Password Authentication" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:95 +#: lib/mv_web/components/layouts/navbar.ex:106 #, elixir-autogen, elixir-format msgid "Profil" msgstr "" @@ -370,7 +380,7 @@ msgstr "" msgid "Select member" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:99 +#: lib/mv_web/components/layouts/navbar.ex:110 #, elixir-autogen, elixir-format msgid "Settings" msgstr "" @@ -538,14 +548,14 @@ msgstr "" msgid "Back to users list" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:33 -#: lib/mv_web/components/layouts/navbar.ex:39 +#: lib/mv_web/components/layouts/navbar.ex:44 +#: lib/mv_web/components/layouts/navbar.ex:50 #, elixir-autogen, elixir-format, fuzzy msgid "Select language" msgstr "" -#: lib/mv_web/components/layouts/navbar.ex:46 -#: lib/mv_web/components/layouts/navbar.ex:66 +#: lib/mv_web/components/layouts/navbar.ex:57 +#: lib/mv_web/components/layouts/navbar.ex:77 #, elixir-autogen, elixir-format msgid "Toggle dark mode" msgstr "" @@ -717,6 +727,7 @@ msgstr "" msgid "Manage global settings for the association." msgstr "" +#: lib/mv_web/live/contribution_settings_live.ex:102 #: lib/mv_web/live/global_settings_live.ex:56 #, elixir-autogen, elixir-format, fuzzy msgid "Save Settings" @@ -854,8 +865,440 @@ 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 3222825..5ba5eb0 100644 --- a/test/mv_web/live/profile_navigation_test.exs +++ b/test/mv_web/live/profile_navigation_test.exs @@ -90,8 +90,6 @@ defmodule MvWeb.ProfileNavigationTest do # Verify we're on the correct profile page with OIDC specific information {:ok, _profile_view, html} = live(conn, "/users/#{user.id}") assert html =~ to_string(user.email) - # OIDC ID should be visible - assert html =~ "oidc_123" # Password auth should be disabled for OIDC users assert html =~ "Not enabled" end 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 0485f5e..802cc8f 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 readable format - assert html =~ "1990" or html =~ "1990-05-15" or html =~ "15.05.1990" + # Date should be displayed in European format (dd.mm.yyyy) + assert html =~ "15.05.1990" end test "formats email custom field values correctly", %{conn: conn, member1: _member1} do diff --git a/test/mv_web/user_live/index_test.exs b/test/mv_web/user_live/index_test.exs index c0b0275..360ef72 100644 --- a/test/mv_web/user_live/index_test.exs +++ b/test/mv_web/user_live/index_test.exs @@ -33,8 +33,6 @@ defmodule MvWeb.UserLive.IndexTest do assert html =~ "alice@example.com" assert html =~ "bob@example.com" - assert html =~ "alice123" - assert html =~ "bob456" end test "shows correct action links", %{conn: conn} do @@ -386,10 +384,6 @@ defmodule MvWeb.UserLive.IndexTest do # Should still show the table structure assert html =~ "Email" - assert html =~ "OIDC ID" - # Should show the authenticated user at minimum - # Matches the generated email pattern oidc.user{unique_id}@example.com - assert html =~ "oidc.user" end test "handles users with missing OIDC ID", %{conn: conn} do