Compare commits

..

2 commits

Author SHA1 Message Date
ad51a226f7
feat: add user to member linking
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-13 22:31:32 +01:00
0135dafa3a
feat: add custom field slug
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-13 19:18:00 +01:00
10 changed files with 135 additions and 4777 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,506 +0,0 @@
# Roles and Permissions - Architecture Overview
**Project:** Mila - Membership Management System
**Feature:** Role-Based Access Control (RBAC) with Hardcoded Permission Sets
**Version:** 2.0
**Last Updated:** 2025-11-13
**Status:** Architecture Design - MVP Approach
---
## Purpose of This Document
This document provides a high-level, conceptual overview of the Roles and Permissions architecture without code examples. It is designed for quick understanding of architectural decisions and concepts.
**For detailed technical implementation:** See [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md)
---
## Table of Contents
1. [Overview](#overview)
2. [Requirements Summary](#requirements-summary)
3. [Evaluated Approaches](#evaluated-approaches)
4. [Selected Architecture](#selected-architecture)
5. [Permission System Design](#permission-system-design)
6. [User-Member Linking Strategy](#user-member-linking-strategy)
7. [Field-Level Permissions Strategy](#field-level-permissions-strategy)
8. [Migration Strategy](#migration-strategy)
9. [Related Documents](#related-documents)
---
## Overview
The Mila membership management system requires a flexible authorization system that controls:
- **Who** can access **what** resources
- **Which** pages users can view
- **How** users interact with their own vs. others' data
### Key Design Principles
1. **Simplicity First:** Start with hardcoded permissions for fast MVP delivery
2. **Performance:** No database queries for permission checks in MVP
3. **Clear Migration Path:** Easy upgrade to database-backed permissions when needed
4. **Security:** Explicit action-based authorization with no ambiguity
5. **Maintainability:** Permission logic reviewable in Git, testable as pure functions
### Core Concepts
**Permission Set:** Defines a collection of permissions (e.g., "read_only", "admin")
**Role:** A named job function that references one Permission Set (e.g., "Vorstand" uses "read_only")
**User:** Each user has exactly one Role, inheriting that Role's Permission Set
**Scope:** Defines the breadth of access - "own" (only own data), "linked" (data connected to user), "all" (everything)
---
## Evaluated Approaches
During the design phase, we evaluated multiple implementation approaches to find the optimal balance between simplicity, performance, and future extensibility.
### Approach 1: JSONB in Roles Table
Store all permissions as a single JSONB column directly in the roles table.
**Advantages:**
- Simplest database schema (single table)
- Very flexible structure
- No additional tables needed
- Fast to implement
**Disadvantages:**
- Poor queryability (can't efficiently filter by specific permissions)
- No referential integrity
- Difficult to validate structure
- Hard to audit permission changes
- Can't leverage database indexes effectively
**Verdict:** Rejected - Poor queryability makes it unsuitable for complex permission logic.
---
### Approach 2: Normalized Database Tables
Separate tables for `permission_sets`, `permission_set_resources`, `permission_set_pages` with full normalization.
**Advantages:**
- Fully queryable with SQL
- Runtime configurable permissions
- Strong referential integrity
- Easy to audit changes
- Can index for performance
**Disadvantages:**
- Complex database schema (4+ tables)
- DB queries required for every permission check
- Requires ETS cache for performance
- Needs admin UI for permission management
- Longer implementation time (4-5 weeks)
- Overkill for fixed set of 4 permission sets
**Verdict:** Deferred to Phase 3 - Excellent for runtime configuration but too complex for MVP.
---
### Approach 3: Custom Authorizer
Implement a custom Ash Authorizer from scratch instead of using Ash Policies.
**Advantages:**
- Complete control over authorization logic
- Can implement any custom behavior
- Not constrained by Ash Policy DSL
**Disadvantages:**
- Significantly more code to write and maintain
- Loses benefits of Ash's declarative policies
- Harder to test than built-in policy system
- Mixes declarative and imperative approaches
- Must reimplement filter generation for queries
- Higher bug risk
**Verdict:** Rejected - Too much custom code, reduces maintainability and loses Ash ecosystem benefits.
---
### Approach 4: Simple Role Enum
Add a simple `:role` enum field directly on User resource with hardcoded checks in each policy.
**Advantages:**
- Very simple to implement (< 1 week)
- No extra tables needed
- Fast performance
- Easy to understand
**Disadvantages:**
- No separation between roles and permissions
- Can't add new roles without code changes
- No dynamic permission configuration
- Not extensible to field-level permissions
- Violates separation of concerns (role = job function, not permission set)
- Difficult to maintain as requirements grow
**Verdict:** Rejected - Too inflexible, doesn't meet requirement for configurable permissions and role separation.
---
### Approach 5: Hardcoded Permissions with Migration Path (SELECTED for MVP)
Permission Sets hardcoded in Elixir module, only Roles table in database.
**Advantages:**
- Fast implementation (2-3 weeks vs 4-5 weeks)
- Maximum performance (zero DB queries, < 1 microsecond)
- Simple to test (pure functions)
- Code-reviewable permissions (visible in Git)
- No migration needed for existing data
- Clearly defined 4 permission sets as required
- Clear migration path to database-backed solution (Phase 3)
- Maintains separation of roles and permission sets
**Disadvantages:**
- Permissions not editable at runtime (only role assignment possible)
- New permissions require code deployment
- Not suitable if permissions change frequently (> 1x/week)
- Limited to the 4 predefined permission sets
**Why Selected:**
- MVP requirement is for 4 fixed permission sets (not custom ones)
- No stated requirement for runtime permission editing
- Performance is critical for authorization checks
- Fast time-to-market (2-3 weeks)
- Clear upgrade path when runtime configuration becomes necessary
**Migration Path:**
When runtime permission editing becomes a business requirement, migrate to Approach 2 (normalized DB tables) without changing the public API of the PermissionSets module.
---
## Requirements Summary
### Four Predefined Permission Sets
1. **own_data** - Access only to own user account and linked member profile
2. **read_only** - Read access to all members and custom fields
3. **normal_user** - Create/Read/Update members and full CRUD on custom fields (no member deletion for safety)
4. **admin** - Unrestricted access to all resources including user management
### Example Roles
- **Mitglied (Member)** - Uses "own_data" permission set, default role
- **Vorstand (Board)** - Uses "read_only" permission set
- **Kassenwart (Treasurer)** - Uses "normal_user" permission set
- **Buchhaltung (Accounting)** - Uses "read_only" permission set
- **Admin** - Uses "admin" permission set
### Authorization Levels
**Resource Level (MVP):**
- Controls create, read, update, destroy actions on resources
- Resources: Member, User, Property, PropertyType, Role
**Page Level (MVP):**
- Controls access to LiveView pages
- Example: "/members/new" requires Member.create permission
**Field Level (Phase 2 - Future):**
- Controls read/write access to specific fields
- Example: Only Treasurer can see payment_history field
### Special Cases
1. **Own Credentials:** Users can always edit their own email and password
2. **Linked Member Email:** Only admins can edit email of members linked to users
3. **User-Member Linking:** Only admins can link/unlink users to members (except self-service creation)
---
## Selected Architecture
### Conceptual Model
```
Elixir Module: PermissionSets
↓ (defines)
Permission Set (:own_data, :read_only, :normal_user, :admin)
↓ (referenced by)
Role (stored in DB: "Vorstand" → "read_only")
↓ (assigned to)
User (each user has one role_id)
```
### Database Schema (MVP)
**Single Table: roles**
Contains:
- id (UUID)
- name (e.g., "Vorstand")
- description
- permission_set_name (String: "own_data", "read_only", "normal_user", "admin")
- is_system_role (boolean, protects critical roles)
**No Permission Tables:** Permission Sets are hardcoded in Elixir module.
### Why This Approach?
**Fast Implementation:** 2-3 weeks instead of 4-5 weeks
**Maximum Performance:**
- Zero database queries for permission checks
- Pure function calls (< 1 microsecond)
- No caching needed
**Code Review:**
- Permissions visible in Git diffs
- Easy to review changes
- No accidental runtime modifications
**Clear Upgrade Path:**
- Phase 1 (MVP): Hardcoded
- Phase 2: Add field-level permissions
- Phase 3: Migrate to database-backed with admin UI
**Meets Requirements:**
- Four predefined permission sets ✓
- Dynamic role creation ✓ (Roles in DB)
- Role-to-user assignment ✓
- No requirement for runtime permission changes stated
---
## Permission System Design
### Permission Structure
Each Permission Set contains:
**Resources:** List of resource permissions
- resource: "Member", "User", "Property", etc.
- action: :read, :create, :update, :destroy
- scope: :own, :linked, :all
- granted: true/false
**Pages:** List of accessible page paths
- Examples: "/", "/members", "/members/:id/edit"
- "*" for admin (all pages)
### Scope Definitions
**:own** - Only records where id == actor.id
- Example: User can read their own User record
**:linked** - Only records where user_id == actor.id
- Example: User can read Member linked to their account
**:all** - All records without restriction
- Example: Admin can read all Members
### How Authorization Works
1. User attempts action on resource (e.g., read Member)
2. System loads user's role from database
3. Role contains permission_set_name string
4. PermissionSets module returns permissions for that set
5. Custom Policy Check evaluates permissions against action
6. Access granted or denied based on scope
### Custom Policy Check
A reusable Ash Policy Check that:
- Reads user's permission_set_name from their role
- Calls PermissionSets.get_permissions/1
- Matches resource + action against permissions list
- Applies scope filters (own/linked/all)
- Returns authorized, forbidden, or filtered query
---
## User-Member Linking Strategy
### Problem Statement
Users need to create member profiles for themselves (self-service), but only admins should be able to:
- Link existing members to users
- Unlink members from users
- Create members pre-linked to arbitrary users
### Selected Approach: Separate Ash Actions
Instead of complex field-level validation, we use action-based authorization.
### Actions on Member Resource
**1. create_member_for_self** (All authenticated users)
- Automatically sets user_id = actor.id
- User cannot specify different user_id
- UI: "Create My Profile" button
**2. create_member** (Admin only)
- Can set user_id to any user or leave unlinked
- Full flexibility for admin
- UI: Admin member management form
**3. link_member_to_user** (Admin only)
- Updates existing member to set user_id
- Connects unlinked member to user account
**4. unlink_member_from_user** (Admin only)
- Sets user_id to nil
- Disconnects member from user account
**5. update** (Permission-based)
- Normal updates (name, address, etc.)
- user_id NOT in accept list (prevents manipulation)
- Available to users with Member.update permission
### Why Separate Actions?
**Explicit Semantics:** Each action has clear, single purpose
**Server-Side Security:** user_id set by server, not client input
**Better UX:** Different UI flows for different use cases
**Simple Policies:** Authorization at action level, not field level
**Easy Testing:** Each action independently testable
---
## Field-Level Permissions Strategy
### Status: Phase 2 (Future Implementation)
Field-level permissions are NOT implemented in MVP but have a clear strategy defined.
### Problem Statement
Some scenarios require field-level control:
- **Read restrictions:** Hide payment_history from certain roles
- **Write restrictions:** Only treasurer can edit payment fields
- **Complexity:** Ash Policies work at resource level, not field level
### Selected Strategy
**For Read Restrictions:**
Use Ash Calculations or Custom Preparations
- Calculations: Dynamically compute field based on permissions
- Preparations: Filter select to only allowed fields
- Field returns nil or "[Hidden]" if unauthorized
**For Write Restrictions:**
Use Custom Validations
- Validate changeset against field permissions
- Similar to existing linked-member email validation
- Return error if field modification not allowed
### Why This Strategy?
**Leverages Ash Features:** Uses built-in mechanisms, not custom authorizer
**Performance:** Calculations are lazy, Preparations run once per query
**Maintainable:** Clear validation logic, standard Ash patterns
**Extensible:** Easy to add new field restrictions
### Implementation Timeline
**Phase 1 (MVP):** No field-level permissions
**Phase 2:** Extend PermissionSets to include field permissions, implement Calculations/Validations
**Phase 3:** If migrating to database, add permission_set_fields table
---
## Migration Strategy
### Phase 1: MVP with Hardcoded Permissions (2-3 weeks)
**What's Included:**
- Roles table in database
- PermissionSets Elixir module with 4 predefined sets
- Custom Policy Check reading from module
- UI Authorization Helpers for LiveView
- Admin UI for role management (create, assign, delete roles)
**Limitations:**
- Permissions not editable at runtime
- New permissions require code deployment
- Only 4 permission sets available
**Benefits:**
- Fast implementation
- Maximum performance
- Simple testing and review
### Phase 2: Field-Level Permissions (Future, 2-3 weeks)
**When Needed:** Business requires field-level restrictions
**Implementation:**
- Extend PermissionSets module with :fields key
- Add Ash Calculations for read restrictions
- Add custom validations for write restrictions
- Update UI Helpers
**Migration:** No database changes, pure code additions
### Phase 3: Database-Backed Permissions (Future, 3-4 weeks)
**When Needed:** Runtime permission configuration required
**Implementation:**
- Create permission tables in database
- Seed script to migrate hardcoded permissions
- Update PermissionSets module to query database
- Add ETS cache for performance
- Build admin UI for permission management
**Migration:** Seamless, no changes to existing Policies or UI code
### Decision Matrix: When to Migrate?
| Scenario | Recommended Phase |
|----------|-------------------|
| MVP with 4 fixed permission sets | Phase 1 |
| Need field-level restrictions | Phase 2 |
| Permission changes < 1x/month | Stay Phase 1 |
| Need runtime permission config | Phase 3 |
| Custom permission sets needed | Phase 3 |
| Permission changes > 1x/week | Phase 3 |
---
## Related Documents
**This Document (Overview):** High-level concepts, no code examples
**[roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md):** Complete technical specification with code examples
**[roles-and-permissions-implementation-plan.md](./roles-and-permissions-implementation-plan.md):** Detailed implementation plan with TDD approach
**[CODE_GUIDELINES.md](../CODE_GUIDELINES.md):** Project coding standards
---
## Summary
The selected architecture uses **hardcoded Permission Sets in Elixir** for the MVP, providing:
- **Speed:** 2-3 weeks implementation vs 4-5 weeks
- **Performance:** Zero database queries for authorization
- **Clarity:** Permissions in Git, reviewable and testable
- **Flexibility:** Clear migration path to database-backed system
**User-Member linking** uses **separate Ash Actions** for clarity and security.
**Field-level permissions** have a **defined strategy** (Calculations + Validations) for Phase 2 implementation.
The approach balances pragmatism for MVP delivery with extensibility for future requirements.

View file

@ -19,6 +19,9 @@ defmodule MvWeb.CustomFieldLive.Form do
- immutable - If true, values cannot be changed after creation (default: false)
- required - If true, all members must have this custom field (default: false)
**Read-only (Edit mode only):**
- slug - Auto-generated URL-friendly identifier (immutable)
## Value Type Selection
- `:string` - Text data (unlimited length)
- `:integer` - Numeric data
@ -49,6 +52,19 @@ defmodule MvWeb.CustomFieldLive.Form do
<.form for={@form} id="custom_field-form" phx-change="validate" phx-submit="save">
<.input field={@form[:name]} type="text" label={gettext("Name")} />
<%!-- Show slug in edit mode (read-only) --%>
<div :if={@custom_field} class="mb-4">
<label class="block text-sm font-semibold leading-6 text-zinc-800">
{gettext("Slug")}
</label>
<div class="mt-2 p-3 bg-zinc-50 border border-zinc-300 rounded-lg text-sm text-zinc-700">
{@custom_field.slug}
</div>
<p class="mt-2 text-sm leading-6 text-zinc-600">
{gettext("Auto-generated identifier (immutable)")}
</p>
</div>
<.input
field={@form[:value_type]}
type="select"

View file

@ -11,6 +11,7 @@ defmodule MvWeb.CustomFieldLive.Index do
- Delete custom fields (if no custom field values use them)
## Displayed Information
- Slug: URL-friendly identifier (auto-generated from name)
- Name: Unique identifier for the custom field
- Value type: Data type constraint (string, integer, boolean, date, email)
- Description: Human-readable explanation
@ -43,6 +44,8 @@ defmodule MvWeb.CustomFieldLive.Index do
rows={@streams.custom_fields}
row_click={fn {_id, custom_field} -> JS.navigate(~p"/custom_fields/#{custom_field}") end}
>
<:col :let={{_id, custom_field}} label="Slug">{custom_field.slug}</:col>
<:col :let={{_id, custom_field}} label="Name">{custom_field.name}</:col>
<:col :let={{_id, custom_field}} label="Description">{custom_field.description}</:col>

View file

@ -50,12 +50,7 @@ defmodule MvWeb.CustomFieldLive.Show do
<.list>
<:item title="Id">{@custom_field.id}</:item>
<:item title="Slug">
{@custom_field.slug}
<p class="mt-2 text-sm leading-6 text-zinc-600">
{gettext("Auto-generated identifier (immutable)")}
</p>
</:item>
<:item title="Slug">{@custom_field.slug}</:item>
<:item title="Name">{@custom_field.name}</:item>

View file

@ -39,7 +39,7 @@ defmodule MvWeb.CustomFieldValueLive.Form do
<.header>
{@page_title}
<:subtitle>
{gettext("Use this form to manage Custom Field Value records in your database.")}
{gettext("Use this form to manage custom_field_value records in your database.")}
</:subtitle>
</.header>

View file

@ -41,7 +41,7 @@ msgid "Delete"
msgstr "Löschen"
#: lib/mv_web/live/member_live/index.html.heex:194
#: lib/mv_web/live/user_live/form.ex:251
#: lib/mv_web/live/user_live/form.ex:247
#: lib/mv_web/live/user_live/index.html.heex:66
#, elixir-autogen, elixir-format
msgid "Edit"
@ -158,10 +158,10 @@ msgstr "Postleitzahl"
msgid "Save Member"
msgstr "Mitglied speichern"
#: lib/mv_web/live/custom_field_live/form.ex:64
#: lib/mv_web/live/custom_field_live/form.ex:80
#: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:234
#: lib/mv_web/live/user_live/form.ex:230
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr "Speichern..."
@ -203,14 +203,14 @@ msgstr "Dies ist ein Mitglied aus deiner Datenbank."
msgid "Yes"
msgstr "Ja"
#: lib/mv_web/live/custom_field_live/form.ex:108
#: lib/mv_web/live/custom_field_live/form.ex:124
#: lib/mv_web/live/custom_field_value_live/form.ex:233
#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "create"
msgstr "erstellt"
#: lib/mv_web/live/custom_field_live/form.ex:109
#: lib/mv_web/live/custom_field_live/form.ex:125
#: lib/mv_web/live/custom_field_value_live/form.ex:234
#: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format
@ -252,10 +252,10 @@ msgstr "Ihre E-Mail-Adresse wurde bestätigt"
msgid "Your password has successfully been reset"
msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
#: lib/mv_web/live/custom_field_live/form.ex:67
#: lib/mv_web/live/custom_field_live/form.ex:83
#: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/member_live/form.ex:82
#: lib/mv_web/live/user_live/form.ex:237
#: lib/mv_web/live/user_live/form.ex:233
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr "Abbrechen"
@ -265,7 +265,7 @@ msgstr "Abbrechen"
msgid "Choose a member"
msgstr "Mitglied auswählen"
#: lib/mv_web/live/custom_field_live/form.ex:60
#: lib/mv_web/live/custom_field_live/form.ex:76
#, elixir-autogen, elixir-format
msgid "Description"
msgstr "Beschreibung"
@ -285,7 +285,7 @@ msgstr "Aktiviert"
msgid "ID"
msgstr "ID"
#: lib/mv_web/live/custom_field_live/form.ex:61
#: lib/mv_web/live/custom_field_live/form.ex:77
#, elixir-autogen, elixir-format
msgid "Immutable"
msgstr "Unveränderlich"
@ -313,7 +313,7 @@ msgstr "Mitglied"
msgid "Members"
msgstr "Mitglieder"
#: lib/mv_web/live/custom_field_live/form.ex:50
#: lib/mv_web/live/custom_field_live/form.ex:53
#, elixir-autogen, elixir-format
msgid "Name"
msgstr "Name"
@ -335,7 +335,7 @@ msgstr "Nicht gesetzt"
#: 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:210
#: lib/mv_web/live/user_live/form.ex:209
#, elixir-autogen, elixir-format
msgid "Note"
msgstr "Hinweis"
@ -356,7 +356,7 @@ msgstr "Passwort-Authentifizierung"
msgid "Profil"
msgstr "Profil"
#: lib/mv_web/live/custom_field_live/form.ex:62
#: lib/mv_web/live/custom_field_live/form.ex:78
#, elixir-autogen, elixir-format
msgid "Required"
msgstr "Erforderlich"
@ -376,7 +376,7 @@ msgstr "Mitglied auswählen"
msgid "Settings"
msgstr "Einstellungen"
#: lib/mv_web/live/user_live/form.ex:235
#: lib/mv_web/live/user_live/form.ex:231
#, elixir-autogen, elixir-format
msgid "Save User"
msgstr "Benutzer*in speichern"
@ -401,7 +401,7 @@ msgstr "Nicht unterstützter Wertetyp: %{type}"
msgid "Use this form to manage user records in your database."
msgstr "Verwenden Sie dieses Formular, um Benutzer*innen-Datensätze zu verwalten."
#: lib/mv_web/live/user_live/form.ex:252
#: lib/mv_web/live/user_live/form.ex:248
#: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "User"
@ -412,7 +412,7 @@ msgstr "Benutzer*in"
msgid "Value"
msgstr "Wert"
#: lib/mv_web/live/custom_field_live/form.ex:55
#: lib/mv_web/live/custom_field_live/form.ex:71
#, elixir-autogen, elixir-format
msgid "Value type"
msgstr "Wertetyp"
@ -429,7 +429,7 @@ msgstr "aufsteigend"
msgid "descending"
msgstr "absteigend"
#: lib/mv_web/live/user_live/form.ex:251
#: lib/mv_web/live/user_live/form.ex:247
#, elixir-autogen, elixir-format
msgid "New"
msgstr "Neue*r"
@ -620,7 +620,7 @@ msgstr "Benutzerdefinierte Feldwerte"
msgid "Custom field"
msgstr "Benutzerdefiniertes Feld"
#: lib/mv_web/live/custom_field_live/form.ex:115
#: lib/mv_web/live/custom_field_live/form.ex:131
#, elixir-autogen, elixir-format
msgid "Custom field %{action} successfully"
msgstr "Benutzerdefiniertes Feld erfolgreich %{action}"
@ -635,7 +635,7 @@ msgstr "Benutzerdefinierter Feldwert erfolgreich %{action}"
msgid "Please select a custom field first"
msgstr "Bitte wähle zuerst ein Benutzerdefiniertes Feld"
#: lib/mv_web/live/custom_field_live/form.ex:65
#: lib/mv_web/live/custom_field_live/form.ex:81
#, elixir-autogen, elixir-format
msgid "Save Custom field"
msgstr "Benutzerdefiniertes Feld speichern"
@ -645,32 +645,32 @@ msgstr "Benutzerdefiniertes Feld speichern"
msgid "Save Custom field value"
msgstr "Benutzerdefinierten Feldwert speichern"
#: lib/mv_web/live/custom_field_live/form.ex:45
#: lib/mv_web/live/custom_field_live/form.ex:48
#, elixir-autogen, elixir-format
msgid "Use this form to manage custom_field records in your database."
msgstr "Verwende dieses Formular, um Benutzerdefinierte Felder in deiner Datenbank zu verwalten."
#: lib/mv_web/live/custom_field_value_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Use this form to manage custom_field_value records in your database."
msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Datenbank zu verwalten."
#: lib/mv_web/components/layouts/navbar.ex:20
#, elixir-autogen, elixir-format
msgid "Custom Fields"
msgstr "Benutzerdefinierte Felder"
#: lib/mv_web/live/custom_field_value_live/form.ex:42
#, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage Custom Field Value records in your database."
msgstr "Verwende dieses Formular, um Benutzerdefinierte Feldwerte in deiner Datenbank zu verwalten."
#: lib/mv_web/live/custom_field_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Auto-generated identifier (immutable)"
msgstr "Automatisch generierte Kennung (unveränderlich)"
#: lib/mv_web/live/user_live/form.ex:210
#: lib/mv_web/live/user_live/form.ex:209
#, elixir-autogen, elixir-format
msgid "A member with this email already exists. To link with a different member, please change one of the email addresses first."
msgstr "Ein Mitglied mit dieser E-Mail-Adresse existiert bereits. Um mit einem anderen Mitglied zu verknüpfen, ändern Sie bitte zuerst eine der E-Mail-Adressen."
#: lib/mv_web/live/user_live/form.ex:185
#: lib/mv_web/live/custom_field_live/form.ex:64
#, elixir-autogen, elixir-format
msgid "Auto-generated identifier (immutable)"
msgstr "Automatisch generierte Kennung (unveränderlich)"
#: lib/mv_web/live/user_live/form.ex:184
#, elixir-autogen, elixir-format
msgid "Available members"
msgstr "Verfügbare Mitglieder"
@ -680,7 +680,7 @@ msgstr "Verfügbare Mitglieder"
msgid "Member will be unlinked when you save. Cannot select new member until saved."
msgstr "Mitglied wird beim Speichern entverknüpft. Neues Mitglied kann erst nach dem Speichern ausgewählt werden."
#: lib/mv_web/live/user_live/form.ex:226
#: lib/mv_web/live/user_live/form.ex:222
#, elixir-autogen, elixir-format
msgid "Save to confirm linking."
msgstr "Speichern, um die Verknüpfung zu bestätigen."
@ -695,11 +695,16 @@ msgstr "Nach einem Mitglied zum Verknüpfen suchen..."
msgid "Search for member to link"
msgstr "Nach Mitglied zum Verknüpfen suchen"
#: lib/mv_web/live/user_live/form.ex:223
#: lib/mv_web/live/user_live/form.ex:219
#, elixir-autogen, elixir-format
msgid "Selected"
msgstr "Ausgewählt"
#: lib/mv_web/live/custom_field_live/form.ex:58
#, elixir-autogen, elixir-format
msgid "Slug"
msgstr "Slug"
#: lib/mv_web/live/user_live/form.ex:143
#, elixir-autogen, elixir-format
msgid "Unlink Member"
@ -709,8 +714,3 @@ msgstr "Mitglied entverknüpfen"
#, elixir-autogen, elixir-format
msgid "Unlinking scheduled"
msgstr "Entverknüpfung geplant"
#~ #: lib/mv_web/live/custom_field_live/form.ex:58
#~ #, elixir-autogen, elixir-format
#~ msgid "Slug"
#~ msgstr "Slug"

View file

@ -42,7 +42,7 @@ msgid "Delete"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:194
#: lib/mv_web/live/user_live/form.ex:251
#: lib/mv_web/live/user_live/form.ex:247
#: lib/mv_web/live/user_live/index.html.heex:66
#, elixir-autogen, elixir-format
msgid "Edit"
@ -159,10 +159,10 @@ msgstr ""
msgid "Save Member"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:64
#: lib/mv_web/live/custom_field_live/form.ex:80
#: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:234
#: lib/mv_web/live/user_live/form.ex:230
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
@ -204,14 +204,14 @@ msgstr ""
msgid "Yes"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:108
#: lib/mv_web/live/custom_field_live/form.ex:124
#: lib/mv_web/live/custom_field_value_live/form.ex:233
#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "create"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:109
#: lib/mv_web/live/custom_field_live/form.ex:125
#: lib/mv_web/live/custom_field_value_live/form.ex:234
#: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format
@ -253,10 +253,10 @@ msgstr ""
msgid "Your password has successfully been reset"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:67
#: lib/mv_web/live/custom_field_live/form.ex:83
#: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/member_live/form.ex:82
#: lib/mv_web/live/user_live/form.ex:237
#: lib/mv_web/live/user_live/form.ex:233
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
@ -266,7 +266,7 @@ msgstr ""
msgid "Choose a member"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:60
#: lib/mv_web/live/custom_field_live/form.ex:76
#, elixir-autogen, elixir-format
msgid "Description"
msgstr ""
@ -286,7 +286,7 @@ msgstr ""
msgid "ID"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:61
#: lib/mv_web/live/custom_field_live/form.ex:77
#, elixir-autogen, elixir-format
msgid "Immutable"
msgstr ""
@ -314,7 +314,7 @@ msgstr ""
msgid "Members"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:50
#: lib/mv_web/live/custom_field_live/form.ex:53
#, elixir-autogen, elixir-format
msgid "Name"
msgstr ""
@ -336,7 +336,7 @@ msgstr ""
#: 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:210
#: lib/mv_web/live/user_live/form.ex:209
#, elixir-autogen, elixir-format
msgid "Note"
msgstr ""
@ -357,7 +357,7 @@ msgstr ""
msgid "Profil"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:62
#: lib/mv_web/live/custom_field_live/form.ex:78
#, elixir-autogen, elixir-format
msgid "Required"
msgstr ""
@ -377,7 +377,7 @@ msgstr ""
msgid "Settings"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:235
#: lib/mv_web/live/user_live/form.ex:231
#, elixir-autogen, elixir-format
msgid "Save User"
msgstr ""
@ -402,7 +402,7 @@ msgstr ""
msgid "Use this form to manage user records in your database."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:252
#: lib/mv_web/live/user_live/form.ex:248
#: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "User"
@ -413,7 +413,7 @@ msgstr ""
msgid "Value"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:55
#: lib/mv_web/live/custom_field_live/form.ex:71
#, elixir-autogen, elixir-format
msgid "Value type"
msgstr ""
@ -430,7 +430,7 @@ msgstr ""
msgid "descending"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:251
#: lib/mv_web/live/user_live/form.ex:247
#, elixir-autogen, elixir-format
msgid "New"
msgstr ""
@ -621,7 +621,7 @@ msgstr ""
msgid "Custom field"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:115
#: lib/mv_web/live/custom_field_live/form.ex:131
#, elixir-autogen, elixir-format
msgid "Custom field %{action} successfully"
msgstr ""
@ -636,7 +636,7 @@ msgstr ""
msgid "Please select a custom field first"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:65
#: lib/mv_web/live/custom_field_live/form.ex:81
#, elixir-autogen, elixir-format
msgid "Save Custom field"
msgstr ""
@ -646,32 +646,32 @@ msgstr ""
msgid "Save Custom field value"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:45
#: lib/mv_web/live/custom_field_live/form.ex:48
#, elixir-autogen, elixir-format
msgid "Use this form to manage custom_field records in your database."
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Use this form to manage custom_field_value records in your database."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:20
#, elixir-autogen, elixir-format
msgid "Custom Fields"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:42
#, elixir-autogen, elixir-format
msgid "Use this form to manage Custom Field Value records in your database."
msgstr ""
#: lib/mv_web/live/custom_field_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Auto-generated identifier (immutable)"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:210
#: lib/mv_web/live/user_live/form.ex:209
#, elixir-autogen, elixir-format
msgid "A member with this email already exists. To link with a different member, please change one of the email addresses first."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:185
#: lib/mv_web/live/custom_field_live/form.ex:64
#, elixir-autogen, elixir-format
msgid "Auto-generated identifier (immutable)"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:184
#, elixir-autogen, elixir-format
msgid "Available members"
msgstr ""
@ -681,7 +681,7 @@ msgstr ""
msgid "Member will be unlinked when you save. Cannot select new member until saved."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:226
#: lib/mv_web/live/user_live/form.ex:222
#, elixir-autogen, elixir-format
msgid "Save to confirm linking."
msgstr ""
@ -696,11 +696,16 @@ msgstr ""
msgid "Search for member to link"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:223
#: lib/mv_web/live/user_live/form.ex:219
#, elixir-autogen, elixir-format
msgid "Selected"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:58
#, elixir-autogen, elixir-format
msgid "Slug"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:143
#, elixir-autogen, elixir-format
msgid "Unlink Member"

View file

@ -42,7 +42,7 @@ msgid "Delete"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex:194
#: lib/mv_web/live/user_live/form.ex:251
#: lib/mv_web/live/user_live/form.ex:247
#: lib/mv_web/live/user_live/index.html.heex:66
#, elixir-autogen, elixir-format
msgid "Edit"
@ -159,10 +159,10 @@ msgstr ""
msgid "Save Member"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:64
#: lib/mv_web/live/custom_field_live/form.ex:80
#: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:234
#: lib/mv_web/live/user_live/form.ex:230
#, elixir-autogen, elixir-format
msgid "Saving..."
msgstr ""
@ -204,14 +204,14 @@ msgstr ""
msgid "Yes"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:108
#: lib/mv_web/live/custom_field_live/form.ex:124
#: lib/mv_web/live/custom_field_value_live/form.ex:233
#: lib/mv_web/live/member_live/form.ex:138
#, elixir-autogen, elixir-format
msgid "create"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:109
#: lib/mv_web/live/custom_field_live/form.ex:125
#: lib/mv_web/live/custom_field_value_live/form.ex:234
#: lib/mv_web/live/member_live/form.ex:139
#, elixir-autogen, elixir-format
@ -253,10 +253,10 @@ msgstr ""
msgid "Your password has successfully been reset"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:67
#: lib/mv_web/live/custom_field_live/form.ex:83
#: lib/mv_web/live/custom_field_value_live/form.ex:77
#: lib/mv_web/live/member_live/form.ex:82
#: lib/mv_web/live/user_live/form.ex:237
#: lib/mv_web/live/user_live/form.ex:233
#, elixir-autogen, elixir-format
msgid "Cancel"
msgstr ""
@ -266,7 +266,7 @@ msgstr ""
msgid "Choose a member"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:60
#: lib/mv_web/live/custom_field_live/form.ex:76
#, elixir-autogen, elixir-format
msgid "Description"
msgstr ""
@ -286,7 +286,7 @@ msgstr ""
msgid "ID"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:61
#: lib/mv_web/live/custom_field_live/form.ex:77
#, elixir-autogen, elixir-format
msgid "Immutable"
msgstr ""
@ -314,7 +314,7 @@ msgstr ""
msgid "Members"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:50
#: lib/mv_web/live/custom_field_live/form.ex:53
#, elixir-autogen, elixir-format
msgid "Name"
msgstr ""
@ -336,7 +336,7 @@ msgstr ""
#: 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:210
#: lib/mv_web/live/user_live/form.ex:209
#, elixir-autogen, elixir-format, fuzzy
msgid "Note"
msgstr ""
@ -357,7 +357,7 @@ msgstr ""
msgid "Profil"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:62
#: lib/mv_web/live/custom_field_live/form.ex:78
#, elixir-autogen, elixir-format
msgid "Required"
msgstr ""
@ -377,7 +377,7 @@ msgstr ""
msgid "Settings"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:235
#: lib/mv_web/live/user_live/form.ex:231
#, elixir-autogen, elixir-format, fuzzy
msgid "Save User"
msgstr ""
@ -402,7 +402,7 @@ msgstr ""
msgid "Use this form to manage user records in your database."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:252
#: lib/mv_web/live/user_live/form.ex:248
#: lib/mv_web/live/user_live/show.ex:34
#, elixir-autogen, elixir-format
msgid "User"
@ -413,7 +413,7 @@ msgstr ""
msgid "Value"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:55
#: lib/mv_web/live/custom_field_live/form.ex:71
#, elixir-autogen, elixir-format
msgid "Value type"
msgstr ""
@ -430,7 +430,7 @@ msgstr ""
msgid "descending"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:251
#: lib/mv_web/live/user_live/form.ex:247
#, elixir-autogen, elixir-format
msgid "New"
msgstr ""
@ -621,7 +621,7 @@ msgstr ""
msgid "Custom field"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:115
#: lib/mv_web/live/custom_field_live/form.ex:131
#, elixir-autogen, elixir-format
msgid "Custom field %{action} successfully"
msgstr ""
@ -636,7 +636,7 @@ msgstr ""
msgid "Please select a custom field first"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:65
#: lib/mv_web/live/custom_field_live/form.ex:81
#, elixir-autogen, elixir-format
msgid "Save Custom field"
msgstr ""
@ -646,32 +646,32 @@ msgstr ""
msgid "Save Custom field value"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:45
#: lib/mv_web/live/custom_field_live/form.ex:48
#, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage custom_field records in your database."
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:42
#, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage custom_field_value records in your database."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:20
#, elixir-autogen, elixir-format, fuzzy
msgid "Custom Fields"
msgstr ""
#: lib/mv_web/live/custom_field_value_live/form.ex:42
#, elixir-autogen, elixir-format, fuzzy
msgid "Use this form to manage Custom Field Value records in your database."
msgstr ""
#: lib/mv_web/live/custom_field_live/show.ex:56
#, elixir-autogen, elixir-format
msgid "Auto-generated identifier (immutable)"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:210
#: lib/mv_web/live/user_live/form.ex:209
#, elixir-autogen, elixir-format
msgid "A member with this email already exists. To link with a different member, please change one of the email addresses first."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:185
#: lib/mv_web/live/custom_field_live/form.ex:64
#, elixir-autogen, elixir-format
msgid "Auto-generated identifier (immutable)"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:184
#, elixir-autogen, elixir-format
msgid "Available members"
msgstr ""
@ -681,7 +681,7 @@ msgstr ""
msgid "Member will be unlinked when you save. Cannot select new member until saved."
msgstr ""
#: lib/mv_web/live/user_live/form.ex:226
#: lib/mv_web/live/user_live/form.ex:222
#, elixir-autogen, elixir-format
msgid "Save to confirm linking."
msgstr ""
@ -696,11 +696,16 @@ msgstr ""
msgid "Search for member to link"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:223
#: lib/mv_web/live/user_live/form.ex:219
#, elixir-autogen, elixir-format, fuzzy
msgid "Selected"
msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:58
#, elixir-autogen, elixir-format
msgid "Slug"
msgstr ""
#: lib/mv_web/live/user_live/form.ex:143
#, elixir-autogen, elixir-format
msgid "Unlink Member"
@ -710,8 +715,3 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Unlinking scheduled"
msgstr ""
#~ #: lib/mv_web/live/custom_field_live/form.ex:58
#~ #, elixir-autogen, elixir-format
#~ msgid "Slug"
#~ msgstr ""